An article about the EVE Online game and stackless Python got me thinking about how to implement coroutines and continuations in C. After a short research, I found a simple implementation (without using longjmp!).
Coroutines can be thought of as cooperative multithreading, sometimes also known as user threads. Two or more procedures can yield the CPU to each other in the middle of their execution, so that they appear to run concurrently. It’s a very well-known feature in more academic programming languages such as Lisp and Scheme which have the Call/CC (call-with-current-continuation) mechanism. A continuation is a way of representing “the rest of the execution” at a certain point of time.
Here’s the C implementation of two functions that run “concurrently”. First, the required headers:
#include <signal.h>
#include <stdio.h>
#include <ucontext.h>
Because the two routines need to cooperate, they need to communicate with each other. I will just use a global variable to signal whose turn it is. The volatile key word is needed to prevent possible compiler optimization. I will explain later.
// Signals which routine should run.
volatile int g_turn;
Let the first routine print the elements of the array {1, 2, 3} each in a line.
void routineOne(ucontext_t* self, ucontext_t* other) {
int numbers[] = {1, 2, 3};
int i;
for (i = 0; i < sizeof(numbers)/sizeof(int); ++i) {
printf("Routine one: %d\n", numbers[i]);
// Call other with current continuation
if (g_turn != 1) {
g_turn = 1;
swapcontext(self, other);
}
}
}
A few things to notice here. ucontext_t is a struct that stores a user thread’s execution state, including it’s stack pointers and instruction pointer. We pass two contexts to the function, one for itself and one for the other routine. This is very similar to the continuation-passing programming style in functional languages such as Haskell. The swapcontext() call stores the current state of execution to the first argument and switches to the state stored in the second argument. Notice that in this function, there is only one assignment to g_turn. The compiler does not know the semantics of swapcontext(). To the compiler, it’s just a function, and there’s no way to know it switches execution context. Therefore the compiler can assume g_turn never changes within this function after the first assignment, and replace all subsequent references to g_turn with the constant 1. That’s why we need to use volatile in the declaration.
We let the second routine print the array `{-1, -2, -3}. It’s completely symmetric to the first one.
void routineTwo(ucontext_t* self, ucontext_t* other) {
int numbers[] = {-1, -2, -3};
int i;
for (i = 0; i < sizeof(numbers)/sizeof(int); ++i) {
printf("Routine two: %d\n", numbers[i]);
if (g_turn != 2) {
g_turn = 2;
swapcontext(self, other);
}
}
}
Unlike Stackless Python and most symbolic or functional languages, C is a stack based language. The main function needs to set up a stack for each user thread before starting them.
int main() {
// Continuations
ucontext_t cont_one;
ucontext_t cont_two;
ucontext_t cont_main;
// one stack for each thread
char stack_one[SIGSTKSZ];
char stack_two[SIGSTKSZ];
// Initialize the coutinuations.
cont_one.uc_link = &cont_main;
cont_one.uc_stack.ss_sp = stack_one;
cont_one.uc_stack.ss_size = sizeof(stack_one);
cont_two.uc_link = &cont_main;
cont_two.uc_stack.ss_sp = stack_two;
cont_two.uc_stack.ss_size = sizeof(stack_two);
getcontext(&cont_one);
makecontext(&cont_one, (void (*)())routineOne, 2, &cont_one, &cont_two);
getcontext(&cont_two);
makecontext(&cont_two, (void (*)())routineTwo, 2, &cont_two, &cont_one);
g_turn = 0;
// Call routineOne with current continuation. Continue from here
// after routineOne finishes.
getcontext(&cont_main);
if (g_turn == 0) {
setcontext(&cont_one);
}
return 0;
}
Run this program and you should see the following output:1
Routine one: 1
Routine two: -1
Routine one: 2
Routine two: -2
Routine one: 3
Routine two: -3
Hopefully you’ve found something interesting in this article. Now an exercise for the reader: If you step through this program in gdb, what do you think will happen? Will gdb get confused? Can you step from main() into routineOne() and then into routineTwo()? Try it.