Go-style concurrency in C
libmill.org
libmill.org
Of course, it uses the longjmp/setjmp trick, as suggested by Dmitry Vyukov.
http://www.1024cores.net/home/lock-free-algorithms/tricks/fi...
However, I'm not sure of the benefit of this approach, compared to writing a few lines of stack switching assembly code per architecture. Especially, how portable are the implementations of longjmp and setjmp across the multitude of C libraries and operating systems?
But more importantly, it doesn't seem to solve the two main issues of coroutines libraries written in C that were already solved in Go:
- How does the stack can grow or shrink? (specifying the stack size in the worse case scenario doesn't scale well)
- How can a coroutine be moved to another operating system thread and back? (useful when blocking or when parallelism is needed)
The way I see it, there are two ways to use a library like this that are worth considering: adapting existing projects, and starting new ones. In my opinion, the gains to be had from rearchitecting an existing project to use such a library are minimal, and for the vast majority new projects, C is not the best choice these days. If you want the performance of C with the programming style of Go, you can probably get away with just using Go!
That said, it's nice to see a polished, working implementation of this idea. Definitely something worth being aware of.
You can do concurrency on a single thread. This is the message I find it hard to get across when explaining CSP concurrency libraries.
Some people argue about usefulness of such libraries as being not native and leaky abstraction. I developed Go-style concurrency "port" to Tcl:
https://github.com/securitykiss-com/csp
and it is one of few developed-in-house things that became the productivity booster in my toolbox.
The interesting thing about it is that thanks to the expressive power of Tcl I was able to port not only the semantics but also almost completely mimic Go syntax.
> You can do concurrency on a single thread. This is the message I find it hard to get across when explaining CSP concurrency libraries.
Heck, there's an entire language built around single-threaded concurrency! Compiles to C, coincidentally - it targets embedded platforms. It's not quite CSP style though, it takes after Esterel and uses synchronous reactive concurrency:
However, as soon as you have multiple Céu programs communicating (for example, multiple Arduino's) the program-to-program communication is fairly CSP-like.
The speedup was caused by replacing setjmp by _setjmp though.
I know this may sound mysterious, but for BSD/OSX... "yes". It is "well known" (if you're the sort of person who writes coroutine runtimes [1]) that _setjmp is far faster than setjmp. It's morally equivalent to 'sigsetjmp', but more portable, if less standard.> If I understand correctly, this is a framework for non-preemptive cooperative multitasking whereby provided API functions serve as yield points. Pretty much what Windows API was before NT/2000. Not exactly a co-routine library, at least not in a conventional definition of co-routines.
> Yes, that's exactly what it is. Yes, that's exactly what it is. But it also matches wikipedia's difinition of coroutine: "Coroutines are computer program components that generalize subroutines for nonpreemptive multitasking, by allowing multiple entry points for suspending and resuming execution at certain locations."
Not necessarily. You can do user-space pre-emptive threading on any system that support pre-emptive signal handlers and that lets you mess around with the stack, particularly if you can generate signals from timers and/or IO operations (both of which you can with POSIX timers)
At some point any user space scheduler will switch to the kernel in an event demultiplexing syscall and potentially block there if no user space thread is runnable. By your definition any threading system which doesn't handle all IO purely in user-space can't claim to be user-space threads.
I don't know enough about userspace/kernelspace communication, maybe I/O (refilling buffers or calling select) and timers are much more lightweight than full thread context switches, but I'm guessing it's still much slower than pure userspace scheduling.
Note that on Unix you can use signals to get IO readiness notification and in fact for sometime on Linux (before epoll) realtime signals were the preferred method to do get notification over a large amount of fds.
Anyway, as a general guideline, user/kernel transition is cheaper than a (kernel) thread/thread transition which is in turn cheaper than a process/process transition.
Of course, doing it in portable C is going to be much slower than raw stack-switching style.
https://github.com/baruch/libwire/wiki/Other-coroutine-libra...
And if you don't like any of those, my next choice would be to use the implementation from Luajit 1.x and 2.x, called Coco but not packaged into a separate library:
It can execute up to 20 million coroutines
and 50 million context switches per second.
Is entirely hardware dependent. Also seems to disregard the fact that certain other things are much easier in go than C.The things that distinguish languages are typically in other dimensions.
that's quite a resume
I'm not an experienced go developer but my understanding is that it's possible in go to have millions of goroutines each blocked on IO. I don't see how a C function could mimic that. I thought stack moving was an essential requirement of any runtime that wants to mimic go in this way.
I've looked at some of the special macros like coroutine and go and it looks like libmill runs the entire function fn when you call go(fn) so if fn is blocked on IO then your entire thread is blocked on IO and you can soon grind to a halt if you have even a few blocked coroutines.
There's a very good reason for this. First of all, you can use file descriptors as locks, semaphores, condition variables, etc using pipe(2) and other functions. On Windows you can use WaitForMultipleObjects, etc on Mutex objects.
But select/epoll/kqueue/WaitForMultipleObjects is a system call that goes in to the kernel. That is very inefficient if you want e.g. mutual exclusion using a mutex. In a modern OS, a mutex (or CriticalSection in Windows) is implemented using a futex ("fast userspace mutex"), which is just a spinlock in userspace that only calls to kernel if the mutex is contended. An uncontended futex can be locked and unlocked in less than 20 nanoseconds. A system call can be 100x slower than that.
Some languages, e.g. Haskell have a green threading system that implements mutexes and i/o in a consistent way with i/o using a userspace scheduler. But this depends on language and runtime implementation.
As for threads, it's single threaded (yet concurrent). If you want to take advantage of multicore machines, there's a step in the tutorial about that: http://libmill.org/tutorial.html#step7
What does libmill do here? Does it allocate a large stack right off the bat? What happens in a stack overflow, does it segfault or will it just trample over whatever is beneath it?
Edit: an ounce of investigation is worth a pound of answered questions, or something like that. It appears stack sizes are configurable, but default to 256kb [1] (for scale, go defaults to 2k), and are retained in a cache of size 64 [#L72] (by default). Also the bottom page is used as a stack guard if posix and mprotect are available [#L90].
From a cursory glance there appears to be some data races in the stack allocation code, though I could be mistaken. E.g. stack.c#L137
[1]: https://github.com/sustrik/libmill/blob/master/stack.c#L47
I've never used libmill but from a glance at the docs it appears they suggest using multiple processes for parallelism, not multiple threads, so there is no such thing as a data race.
As for the default stack size, it's hard (but doable) to work with smaller stack size in C. For example, humble printf() can allocate a buffer several kB long on stack.
#define DISPATCH_SOURCE_TYPE_DATA_ADD
#define DISPATCH_SOURCE_TYPE_DATA_OR
#define DISPATCH_SOURCE_TYPE_MACH_RECV
#define DISPATCH_SOURCE_TYPE_MACH_SEND
#define DISPATCH_SOURCE_TYPE_PROC
#define DISPATCH_SOURCE_TYPE_READ
#define DISPATCH_SOURCE_TYPE_SIGNAL
#define DISPATCH_SOURCE_TYPE_TIMER
#define DISPATCH_SOURCE_TYPE_VNODE
#define DISPATCH_SOURCE_TYPE_WRITE
#define DISPATCH_SOURCE_TYPE_MEMORYPRESSURE• You don't need to use blocks, there are function callback versions of the calls available, but blocks are nicer.
• There are asynchronous IO routines, that covers most of your stalls.
• Go probably stalls threads on page faults, as does libdispatch.
You can read about the Linux port of libdispatch at https://github.com/nickhutchinson/libdispatch
"finally! it's so ridiculously easy now that everyone will start using it and
the world will be roses and meatballs from now on!"
said no one ever.even the plan9 guys who were the first to do it with alef and libthread didn't feel like they'd solved concurrency in C.
Go simply can't interface everything the way C does. Go doesn't give you the same level of flexibility either.
tldr: Go's great, but it's just not as powerful as C (unlike Rust or some others, which can do anything C does).
There are many go libraries I would love to use, but can't because of this. I expect many others are in a similar boat.
I can go on for a while :)