Actually I meant "coroutines".
You create a main infinite loop in your application, in which you call all functions that are meant to perform activities that should be happening "at the same time", like for example taking ADC measurements, while updating a PID control algorithm, while waiting for the GPRS chip to connect to the internet; those would be their own function, called "protothread" if you use the Adam Dunkel's library, and they are going to run a number of blocking operations, waiting for some external hardware to do their thing.
Each one of those functions is converted into a Duff device by means of the macros provided by the Protothreads library.
Now each protothread function can express its own operations in a linear and very clear way, and yield execution to the other functions by just returning from the Duff device. When the main loop starts over, each of those functions will "resume" their operation at the point where they yielded during the last iteration.
The way I see it, this trick is much more than just loop unrolling; it provides a C developer with the same basic features that an async function would have in JS; and the yield points in the functions would be equivalent to the 'await' calls. This allows for very easy to read code, being able to follow the logic of each individual "thread" without much of the noise needed for taking care of the "coroutines" part. The next step (in the embedded world, at least) would be going the route of an RTOS, such as FreeRTOS, but I've managed well so far without it.
EDIT: and yes, this is basically also a neat was of streamlining a state machine