lua-users home
lua-l archive

[Date Prev][Date Next][Thread Prev][Thread Next] [Date Index] [Thread Index]


On Wed, Nov 23, 2016 at 12:59:30AM +0000, Dibyendu Majumdar wrote:
> Hi
> 
> This is just something I have been wondering about. I personally have
> not yet found a use case for Lua's coroutines - the main reason being
> that the resume has to be explicit. What would be more useful is if
> the coroutine semantics could be harnessed to create an async/await
> type solution as in C#. That is, can a solution be found to
> automatically resume the coroutine when an event associated with a
> 'yield' completes. As Lua is single threaded so the solution needs to
> resume the coroutine within the same OS thread.

For some definition of "automatic", this is actually quite trivial.
Obviously it cannot be too automatic because Lua has a contract with C code.
But this can be implemented quite transparently from the perspective of
Lua-script space. The biggest caveat is that Lua objects are multithread
safe.

> I haven't really tried solving this but I thought it might be a useful
> enhancement to Lua if a solution could be found. I imagine that a
> background OS thread will be needed to monitor completion of events
> and then some coordination with the VM will be necessary to resume
> suspended coroutines, maybe whenever the VM does an operation that
> requires system resources. In some ways the 'go' language works like
> this - i.e. there is a background scheduler that swaps in suspended
> goroutines to the thread whenever execution would otherwise pause.

Goroutines work more like Lua coroutines than you'd think. While the kernel
threads that goroutines execute on are preemptively scheduled, AFAIU the Go
goroutine scheduler is cooperative. Control is usually passed between
goroutines when doing reads and writs to a channel. The Go compiler also
inserts checkpoints in compiled code, usually at function call points, to
support garbage collection. Scheduling might also occur at those points.

So while goroutines _feel_ preemptively scheduled, they're actually
cooperative scheduled underneath the hood. It's why loops in Go code that
don't do any I/O, polling, or message passing can hog the entire kernel
thread and stop progress of other goroutines as well as the garbage
collector.

Lua's coroutines can be used in much the same way, you just need to
implement a scheduler much like Go does. Lua gives you the tools to
implement those patterns, you just need to build an application framework
around to make it feel transparent.

I might be mistaken, but C# I suspect async/await is implemented much the
way. Because C# doesn't support coroutines, you have to annotate routines
with async. In the vocabulary of Lua, calling a function marked as async in
C# is like calling a function using coroutine.wrap in Lua. In both cases the
stack frame of the invocation is put onto a separate stack independent from
the caller. In both cases, when the routine "yields" a value, it's state is
kept on the stack. The only real difference is that

1) Lua doesn't support sharing objects between kernel threads, so you can't
yield a value that will be used in another kernel thread; and

2) Lua code can yield from recursively invoked functions, whether or not the
routine was annotated. Whereas in C# (and Python and JavaScript) you can
only yield from an annotated function. Lua's coroutines are actually more
powerful and flexible. The thread-safety limitation follows from the
implemenentation of the VM, not from coroutines generally. C# doesn't
support real coroutines likely because, much like many other virtual
machines, it implements recursion internally on top of the so-called
"C stack", whereas in Lua the VM stack is independent from the C stack.
Because of legacy ABI requirements, the C stack must be very large, so it
would be inefficient to allocate a new C stack for every invocation of an
async routine. A Lua coroutine, by contrast, uses dynamic memory for the
VM's stack and it can be just as efficient or more efficient than the data
structure C# has to allocate for async invocations, especially when you have
chains of async invocations.

Goroutines don't need to support the platform's native ABI issues. Compiled
Go code can initialize a new goroutines with a tiny stack that can grow
dynamically just like Lua. When calling out into C code, Go will invoke the
routine directly on the kernel thread's C stack, and copy the result back
onto the stack of the goroutine. That's why calling C code from Go is slow
and problematic in highly concurrent Go code. If 10,000 goroutines all
invoked a C function, the Go schedule would be forced to create 10,000
kernel threads (each with a multi-megabyte stack), or serialize access to a
smaller number of dedicated kernel threads.

> This is just me thinking aloud, so please feel free to shoot it down,
> or point me to existing solutions that achieve this (I imagine
> projects that integrate libuv might be doing this already).
> 
> Thanks and Regards
> Dibyendu