Protothreads: Lightweight Stackless Threads in C
dunkels.com
dunkels.com
I would highly recommend using an RTOS instead. ;)
See, MCUs are much more interrupt driven than today's PCs, when it comes to I/O, and don't have to run interface logic as it's usually done in hardware.
You can make a widget feel quite snappy and interactive even with very simple linear code.
For the few tasks genuinely requiring real parallelism, or programming fancy multicore MCUs, it will likely be that you will need more than just thread separation, and it is better to do it properly than use hacks — in other world, use process separation and let complexities be handled by the OS.
The callbacks in this case might be the literal interrupt handlers (in which case your dispatcher "loop" is actually the hardware). Or else you make your interrupt write a little bit of state that then gets dispatched by your actual event loop.
Either way, you write responsive software by doing everything in short-running callbacks.
I find it amusing that (some) sophisticated GUI and server framework for big computers re-invent this.
We call them "interrupt handlers." :)
For reasons Alex explains, interrupt handlers have to be really minimal. So you often use them to set state that makes the main loop branch off into the "real" handler.
But even then, you often don't want to wait a long time in that handler either. That too becomes a short-running event handler (though not necessarily a callback). Just not as extremely short running as it would have to be if did it in the interrupt handler.
Reminds me of something -
I used to work for an embedded shop where they used exception-like error handling that was implemented through some basic C macro fuckery. It made the code markedly more succinct, had next to zero overhead and it was just a fantastic piece of work in general. 20 years later and I still remember it fondly.
I'm reading through the comments here and it is difficult to even address some of them, because there is apparent confusion as to the meaning of the word "thread".
In embedded you often do not need or even want threads. What you need is a way to deal with events (mostly interrupts) without losing your sanity. A reasonable approach is to have a state machine that reacts to events, and all interrupt handlers only inject events into the state machine. The state machine is the only "thread" that runs: the CPU runs it until its done processing the current event, and then goes to sleep. Interrupt handlers do as little as possible, basically just drop events into a queue (or even a single slot) for the state machine to process.
This works fine for simple state machines, but quickly gets tedious and complex, especially if you have tasks that involve several sequential events (initialize peripherals, setup ADC, read from ADC, calculate parameters, setup an output device based on parameters computed from ADC reads).
A very good way to hide the state machine is to use CSP (you might have seen it in the Go language or in Clojure as core.async). This lets you write the same thing using certain primitives (channels, blocking and non-blocking reads and writes). Note that until this point this has nothing to do with threads! Clojure's core.async works just as well in Clojurescript, which compiles down to JavaScript and runs in a single thread!
I2C and the peripheral we were dealing with is quite stateful, and writing the access code in a synchronous fashion wasn't possible due to overrunning other constraints. It seemed like a good idea at the time. The sibling comment has it pretty right... it would probably have been quicker in the end to go to an RTOS.
That said, I'm still fairly proud of the I2C state machine that runs in the interrupt handler– it was completely separate from the protothread code that layered over the top.
It's in a few microcontrollers in products we do, the funkiest is the smallest standard AVR package you can get, which imitated an obsolete tiny I/O expander chip we had to replace on a 6mm wide PCB. That was using I2C slave mode, but we could then run the entire sensor interfacing on that board rather than using a separate micro.
It could as well rely on local variables with static lifetime, though.
> If you want multiple threads executing the same function, it is not going to work.
That's true for the example, but in general the solution is to pass the protothreads the thread-local state in a struct.
> It could as well rely on local variables with static lifetime, though.
For those following along, static local variables are the same thing as static global variables[1], C just enforces a scoping restriction as to who can access it at compile time. Most compilers will emit the same BSS section entries for both (though they will mangle the symbol name). It's basically compiler syntactic sugar for most compilers (though I'm sure the actual standard itself allows for some weird exotic use cases where they get handled completely differently).
[1] Static global variables are symbols that are restricted to a compilation unit (as opposed to regular global variables that can be linked to from another compilation unit). If you run `nm` on them, they'll all have a lower case letter indicating it's type.
On GCC and Clang though, labels-as-values allows a cleaner implementation: https://github.com/Akagi201/protothreads/blob/master/include...
- boost::Fiber for C++: https://www.boost.org/doc/libs/1_70_0/libs/fiber/doc/html/in...
- boost::coroutine2 for C++: https://www.boost.org/doc/libs/1_70_0/libs/coroutine2/doc/ht... (lower level than boost::Fiber, and doesn't include an internal scheduler)
- libtask for C: https://swtch.com/libtask/
In the C++ context, this doc summarizes the distinction between "fibers" and "coroutines", although the terminology is not consistent across language ecosystems: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n402...
Not because of what they do (it's a neat trick), but because it teaches bad development practices to programmers.
I had a junior programmer "correct" me on some embedded code saying that all my local variables should actually be declared static, because in their experience "static is the way to go".
This "embedded" system was actually running Linux, using proper threading semantics.
I think it is simply to be expected that junior programmers will frequently pick the wrong tool for the job, for any job in any context, as they have not yet had time to master many tools and do not yet have enough experience to understand the edges of their ignorance. (Presumably we would start thinking of them as peers and calling them "senior programmers" at some point along this journey of growth...)