libco Source Notes (2): Main Structures and Functions
In the previous post, libco Source Notes (1): Coroutines and Context Switching, we covered the basic concept of coroutines and the core context-switching code in libco. This post uses an example to introduce several important function interfaces that libco provides. I suggest reading it together with my annotated version.
Main libco Structures
First, we look at the three core structures in libco. Figure 1 below shows how they relate to each other.
coctx_t
It saves the context information needed when switching coroutines. For details, see libco Source Notes (1): Coroutines and Context Switching. I do not repeat them here.
stCoRoutine_t
This is the main coroutine structure. It holds all the information about a single coroutine, such as its start/stop state, its function, its context, and its shared stack.
stCoRoutineEnv_t
static __thread stCoRoutineEnv_t* gCoEnvPerThread = NULL; //Coroutine runtime environment. __thread: thread-private
This is a thread-private global static variable. It holds the global coroutine environment, such as the coroutine call stack and the epoll handle. pCallStack is the coroutine call stack of the current thread. Because libco uses asymmetric coroutines
Figure 1. libco core structures
Main libco Interface Functions
/* Coroutine creation interface
* @param
* co :double pointer to the main coroutine structure
* attr :configurable coroutine attributes, including stack size and shared stack address
* pfn :function the coroutine calls
* arg :argument of the function the coroutine calls
* @return :0
*/
int co_create( stCoRoutine_t **ppco,const stCoRoutineAttr_t *attr,pfn_co_routine_t pfn,void *arg )
{
if( !co_get_curr_thread_env() )
{
co_init_curr_thread_env(); //Initialize the environment of this thread. Only the main coroutine calls this
}
stCoRoutine_t *co = co_create_env( co_get_curr_thread_env(), attr, pfn,arg ); //Create the coroutine runtime environment and initialize the coroutine data
*ppco = co;
return 0;
}
co_create does two things. First, if the current thread has not initialized its runtime environment stCoRoutineEnv_t, it initializes it. This includes initializing the coroutine call stack, creating the main coroutine, and pushing it onto the stack. Second, it creates a coroutine from the attr parameter, allocates a private stack (or sets up the shared stack), and returns the handle co:
/* Coroutine switch-in interface
* @param
* co :pointer to the main coroutine structure
*/
void co_resume( stCoRoutine_t *co )
{
stCoRoutineEnv_t *env = co->env;
stCoRoutine_t *lpCurrRoutine = env->pCallStack[ env->iCallStackSize - 1 ]; //The coroutine that is currently running
if( !co->cStart ) //First time entering
{
coctx_make( &co->ctx,(coctx_pfn_t)CoRoutineFunc,co,0 ); //Save the context (current registers) in co->ctx
co->cStart = 1; //Mark as started
}
env->pCallStack[ env->iCallStackSize++ ] = co; //Push onto the coroutine call stack
co_swap( lpCurrRoutine, co ); //Switch
}
co_resume switches to a given coroutine. If co has not started yet, it initializes the coroutine stack with coctx_make. Then it pushes the coroutine onto the call stack and switches context with the current coroutine:
void co_yield_env( stCoRoutineEnv_t *env )
{
stCoRoutine_t *last = env->pCallStack[ env->iCallStackSize - 2 ];
stCoRoutine_t *curr = env->pCallStack[ env->iCallStackSize - 1 ];
env->iCallStackSize--;
co_swap( curr, last);
}
/* Interface to switch out the current coroutine
*/
void co_yield_ct()
{
co_yield_env( co_get_curr_thread_env() );
}
/* Coroutine switch-out interface
* @param
* co :pointer to the main coroutine structure
*/
void co_yield( stCoRoutine_t *co )
{
co_yield_env( co->env );
}
The co_yield family of functions makes the current coroutine give up the CPU. They pop it off the call stack and switch context with the previous coroutine on the stack.
Example
//example_test.cpp
#include <stdio.h>
#include <stdlib.h>
#include "co_routine.h"
void* f(void* args) {
while (1) {
printf("f\n");
co_yield_ct();
}
return NULL;
}
void* g(void* args) {
while (1) {
printf("g\n");
co_yield_ct();
}
return NULL;
}
int main() {
stCoRoutine_t* co_f;
stCoRoutine_t* co_g;
co_create(&co_f, NULL, f, NULL);
co_create(&co_g, NULL, g, NULL);
while(1) {
co_resume(co_f);
co_resume(co_g);
}
return 0;
}
Using the three basic functions above, I wrote a small example here. The program creates two coroutines, f and g. Each coroutine prints its own function name and then gives up the CPU. The main coroutine calls co_resume in a loop to wake the two coroutines in turn. The program prints this in a loop:
./example_test
f
g
f
...
Conclusion
We have now covered the core functions of libco and how they execute. Thank you for reading. If you have any questions or thoughts, or find any mistake in this post, please let me know.