(the post is automatically translated by AI)
Introduction
In the previous article, Why Coroutine? (Part 1) — Is Multithreading Not Enough?, we covered the underlying principles of Coroutines and listed the interface we need to implement one in C. In this article, we’ll walk through the code line by line.
Source code: GanniPiece/SimpleCCoroutine: A simple coroutine example implemented using C (github.com)
Background Knowledge
Before we begin, if you’re not familiar with Coroutines, please read the previous article first. In it we mentioned that we can manipulate ucontext_t [1] using four operations defined in <ucontext.h>:
- makecontext [2]
- setcontext [3]
- getcontext [4]
- swapcontext [5]
If you’re already familiar with these, skip ahead to the Implementation section.
ucontext_t
ucontext_t is defined in <ucontext.h> — its full name is “user context.” The header uses typedef to define mcontext_t, and also defines the ucontext_t struct, which contains the following members:
ucontext_t *uc_link pointer to the context that will be resumed
when this context returns
sigset_t uc_sigmask the set of signals that are blocked when this
context is active
stack_t uc_stack the stack used by this context
mcontext_t uc_mcontext a machine-specific representation of the saved
context
uc_link: Pointer to the context to return to when this context exits.uc_sigmask: Signals blocked while this context is active.uc_stack: Stores the function stack and program counter.uc_mcontext: Machine-specific context representation.
getcontext / setcontext
#include <ucontext.h>
int getcontext(ucontext_t *ucp);
int setcontext(const ucontext_t *ucp);
getcontext() initializes the struct pointed to by ucp with the current thread’s context. setcontext() restores the context pointed to by ucp.
makecontext / swapcontext
#include <ucontext.h>
void makecontext(ucontext_t *ucp, (void *func)(), int argc, ...);
int swapcontext(ucontext_t *oucp, const ucontext_t *ucp);
makecontext() specifies the function func for the context pointed to by ucp. When this context is resumed via swapcontext() or setcontext(), argc arguments are passed and func is executed.
Implementation
Step 0: Preprocessor Directives and Macro Definitions
#define _XOPEN_SOURCE
#include <ucontext.h>
#include <stdlib.h>
#include <stdio.h>
#define handle_error(msg) \
do {perror(msg); exit(EXIT_FAILURE); } while(0)
Step 1: Define the Consumer
A consumer has its own task to complete and may be interrupted (yield) during execution.
typedef struct Coroutine {
ucontext_t *ctx;
char func_stack[16384];
void* param;
bool is_finished;
struct Coroutine *next;
} coroutine_t;
Step 2: Define the Producer
The producer is the main Coroutine — it can register consumers and schedule them.
typedef struct Registry {
ucontext_t *ctx;
char func_stack[16384];
coroutine_t *cur_coroutine; // currently running coroutine
coroutine_t *coroutines; // list of coroutines
} co_registry_t;
Step 3: Define the Producer’s Task
static co_registry_t *registry;
static ucontext_t main_ctx;
static void
main_ctx_function ()
{
printf("main context: get control\n");
if (!registry->cur_coroutine) return;
// scheduling
while (1) {
coroutine_t* cur = registry->cur_coroutine;
coroutine_t* ptr = cur->next;
while (ptr->is_finished) {
ptr = ptr->next;
if (ptr == cur) break;
}
if ((cur->is_finished) && (ptr == cur)) break;
registry->cur_coroutine = ptr;
printf("main coroutine: switch\n");
if (swapcontext(registry->ctx, registry->cur_coroutine->ctx) == -1)
handle_error("swapcontext");
}
}
Step 4: Create the Producer’s Context
co_registry_t *
create_producer ()
{
co_registry_t *p;
p = malloc(sizeof(co_registry_t));
p->ctx = malloc(sizeof(ucontext_t));
if (getcontext(p->ctx) == -1)
handle_error("getcontext");
p->ctx->uc_stack.ss_sp = p->func_stack;
p->ctx->uc_stack.ss_size = sizeof(p->func_stack);
makecontext(p->ctx, main_ctx_function, 0);
return p;
}
Step 5: Create a Consumer’s Context
/* create a new coroutine for a new task */
coroutine_t *
create_coroutine(coroutine_body_t func, void *param)
{
coroutine_t *p;
p = malloc(sizeof(coroutine_t));
p->ctx = malloc(sizeof(ucontext_t));
if (getcontext(p->ctx) == -1)
handle_error("getcontext");
p->ctx->uc_stack.ss_sp = p->func_stack;
p->ctx->uc_stack.ss_size = sizeof(p->func_stack);
p->param = param;
p->is_finished = false;
p->next = NULL;
makecontext(p->ctx, func, 0);
return p;
}
Step 6: Implement yield
/* return control to the producer, which will
* determine the next context to run.
*/
void
yield()
{
swapcontext(registry->cur_coroutine->ctx, registry->ctx);
}
Step 7: Register a Coroutine
void
register_coroutine(coroutine_t *coroutine)
{
if (!registry->coroutines)
{
registry->coroutines = coroutine;
registry->cur_coroutine = coroutine;
}
else
{
coroutine_t *ptr = registry->coroutines;
while (ptr->next != NULL) ptr = ptr->next;
ptr->next = coroutine;
}
}
Step 8: Main Program
int main () {
registry = create_co_registry();
printf("creating coroutines...\n");
coroutine_t *co_1 = create_coroutine(func1, (void*) 0);
coroutine_t *co_2 = create_coroutine(func2, (void*) 0);
printf("registering coroutines...\n");
register_coroutine(co_1);
register_coroutine(co_2);
co_2->next = co_1;
printf("starting coroutines...\n");
if (swapcontext(&main_ctx, registry->ctx) == -1)
handle_error("swapcontext");
free(registry);
free(co_1);
free(co_2);
}
Conclusion
In this article, we continued from Part 1 and walked through how to implement a Coroutine in C with actual code. The source code is available at GanniPiece/SimpleCCoroutine — feel free to download and explore it.