Concurrency library for C (coroutines)
Neco is a C library that provides concurrency using coroutines. It's small & fast, and intended to make concurrent I/O & network programming easy.
For a deeper dive, check out the API reference.
It may also be worthwhile to see the Bluebox project for a more complete example of using Neco, including benchmarks.
It's a non-goal for Neco to provide a scalable multithreaded runtime, where the coroutine scheduler is shared among multiple cpu cores. Or to use other concurrency models like async/await.
Just drop the "neco.c" and "neco.h" files into your project. Most modern C compilers should work.
cc -c neco.cA coroutine is started with the neco_start() function.
When neco_start() is called for the first time it will initialize a Neco runtime and scheduler for the current thread, and then blocks until the coroutine and all child coroutines have terminated.
#include <stdio.h>
#include "neco.h"
void coroutine(int argc, void *argv[]) {
printf("main coroutine started\n");
}
int main(int argc, char *argv[]) {
neco_start(coroutine, 0);
return 0;
}Optionally, neco_main() can be used in place of the standard main().
This is for when the entirety of your program is intended to be run from only coroutines.
It adjusts the behavior of the program slightly to make development and error checking easier.
#include <stdio.h>
#include "neco.h"
int neco_main(int argc, char *argv[]) {
printf("main coroutine started\n");
return 0;
}Here we'll start two coroutines that continuously prints "tick" every one second and "tock" every two.
#include <stdio.h>
#include "neco.h"
void ticker(int argc, void *argv[]) {
while (1) {
neco_sleep(NECO_SECOND);
printf("tick\n");
}
}
void tocker(int argc, void *argv[]) {
while (1) {
neco_sleep(NECO_SECOND*2);
printf("tock\n");
}
}
int neco_main(int argc, char *argv[]) {
neco_start(ticker, 0);
neco_start(tocker, 0);
// Keep the program alive for an hour.
neco_sleep(NECO_HOUR);
return 0;
}A coroutine is like its own little program that accepts any number of arguments.
void coroutine(int argc, void *argv[])The arguments are a series of pointers passed to the coroutine.
All arguments are guaranteed to be in scope when the coroutine starts and until the first neco_ function is called. This allows you an opportunity to validate and/or copy them.
…A channel is a mechanism for communicating between two or more coroutines.
Here we'll create a second coroutine that sends the message 'ping' to the first coroutine.
…A generator is like channel but is stricly bound to a coroutine and is intended to treat the coroutine like an iterator.
…Neco provides neco_dial() for easily connecting
to server.
Here we'll performing a (very simple) HTTP request which prints the homepage of the http://example.com website.
…Use neco_serve() to quickly bind and listen on an address.
Here we'll run a tiny webserver at http://127.0.0.1:8080
…Run server with:
cc neco.c echo-server.c && ./a.outRun client with:
cc neco.c echo-client.c && ./a.outecho-server.c
…echo-client.c
…Any coroutines can suspended itself indefinetly and then be resumed by other
coroutines by using neco_suspend() and
neco_resume().
…You can find more examples here.
Linux, Mac, and FreeBSD supports all features.
Windows and WebAssembly support the core coroutine features, but have some key
limitiations, mostly with working with file descriptors and networking.
This is primarly because the Neco event queue works with epoll and kqueue,
which are only available on Linux and Mac/BSD respectively. This means that the
neco_wait() (which allows for a coroutine to wait for a file descriptor to be
readable or writeable) is not currently available on those platforms.
Other limitations include:
Other than that, Neco works great on those platforms.
Any contributions towards making Windows and WebAssembly feature complete are welcome.
Neco uses sco, which is a fair and deterministic scheduler. This means that no coroutine takes priority over another and that all concurrent operations will reproduce in an expected order.
The coroutine context switching is powered by llco and uses assembly code in most cases. On my lab machine (AMD Ryzen 9 5950X) a context switch takes about 11 nanoseconds.
There can be no more than one scheduler per thread.
When the first coroutine is started using neco_start(), a new Neco
runtime is initialized in the current thread, and each runtime has its own
scheduler.
Communicating between coroutines that are running in different threads will
require I/O mechanisms that do not block the current schedulers, such as
pipe(), eventfd() or atomics.
Pthread utilties such as pthread_mutex_t and pthread_cond_t do not work very well in coroutines.
For example, here we'll create two threads, running their own Neco schedulers. Each using pipes to communicate with the other.
…Source code is available under the MIT License.
No open issues yet, or sync has not completed.