Hacker News new | past | comments | ask | show | jobs | submit login

I manage a Lua framework that wraps Linux epoll, BSD kqueue, and Solaris Ports. Lua provides asymmetric coroutines, which the framework uses as logical "threads" for execution state. A Lua coroutine is a just a couple hundred bytes of state, likely smaller than a JavaScript/NodeJS closure all things considered, though slightly larger than a Lua closure.

Lua also has very clean and elegant bindings to C. Lua is designed to be implemented in strict ISO C, yet also to make the C API a first-class citizen--not a hack around the VM like Python, Ruby, etc. The one caveat to this equivalence is coroutines--when a coroutine yields and resumes, it can't revive any C invocation frames (e.g. when Lua calls C calls Lua). So the C API to invoke a Lua routine can take a callback and cookie. If the VM yields and then resumes a coroutine across a C API boundary, the "return" from the Lua routine invocation happens by invoking the C callback. (See lua_callk in the manual.)

From the perspective of C, as well as the kernel, this is a classic asynchronous I/0 pattern. From the perspective of Lua-script code, everything is transparent.

I take it that because Go implements light-weight threads (goroutines, which is likely a pun on coroutines) you do not perceive it as offering async I/O. And yet from the perspective of the implementation as well as the kernel it's classic async I/O using epoll or kqueue, whether goroutines are bound to a single CPU core or not. The send and receive operations on a Go "channel" are very similar to the resume and yield operations of classic routines, and semantically identical in the context of async I/O programming (because with both goroutines and async I/O there are no guarantees about the order of resumption).

Do you equate async I/O with callback-style programming? Do you think callback-style is somehow intrinsically less costly in terms of CPU or memory? I would dispute both of those contentions.




I think the difference is in how people define "async I/O". When saying "Go doesn't use async I/O", what that really expands to is: "While Go can use epoll[0], it doesn't abstract over epoll optimally." i.e. There is a difference between "zero-overhead async-I/O" and goroutines.

I'm not an expert but I'll try to describe why goroutines would have overhead (Someone correct me):

The posterboys of async I/O are Nginx and Redis. I'm probably simplifying, but this is the basics of how they work are: When using epoll directly, the optimal way to store state per connection is using a state machine. The state machine is usually some C-like struct which the compiler can give a fixed size. Each state machine is then constructed to have X memory, and is unable to grow. In theory (I don't believe anyone actually does this), if the program was comfortable with some fixed connection limit, you could fit all of these state machines on the program's stack, and require no heap allocations.

Meanwhile, Go's routines have stacks which can grow. Each goroutine has some max size, and some initial size, both of which are pre-set by the Go-lang team (I'm sure they are configurable). Since the stack can grow: They have to be heap allocated, and need to be either segmented or copied when they need more space[1]. Additionally, there is "internal fragmentation" because a growable stack needs to be consistently overallocated, which is a "waste" of memory.

Very quick Googling suggests that Lua has growable stacks as well.

[0] FWIW: Go could use M:N scheduling of kernel threads to achieve goroutines. Which is another reason why saying goroutines are async I/O could be incorrect. I don't know how its actually implemented.

[1] https://blog.cloudflare.com/how-stacks-are-handled-in-go/




Join us for AI Startup School this June 16-17 in San Francisco!

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: