Will young people implement a coroutine in C++?

Foreword : I came into contact with the concept of coroutine a few days ago and found it very interesting. Because I can use one thread to implement a multi-threaded program, if I use a coroutine instead of a thread, I can save a lot of atomic operations and memory barriers, and greatly reduce system calls related to thread synchronization. Because I only have one thread, and the switch between coroutines can be determined by the function itself.

Insert picture description here

1. Introduction

I have seen several implementations of coroutines. Because there is no native support for C/C++, most libraries use assembly code, and some libraries use C language setjmp and longjmp, but require the use of static local variables in the function. Save the data inside the coroutine. I hate writing assembly and using static local variables, so I came up with a slightly more elegant and clever way to implement it. This article will show you the basic principles and implementation of this method.
Insert picture description here

2. Basic principles

The biggest difficulty in C/C++ implementation is to create, save and restore the context of the program. Because this involves the management of the program stack and the access of CPU registers, but these two contents are not strictly defined in the C/C++ standard, so it is impossible for us to have a completely cross-platform C/C++ implementation. But using the API provided by the operating system, we can still avoid the use of assembly code. Next, we will show you a simple coroutine framework implemented using POSIX's pthread. what! ? ? Pthread? Isn't your program multithreaded? Is that still called a coroutine! Yes, it is indeed multi-threaded, but only for a short moment before the coroutine is created.

To create the context of the subroutine, we can call the pthread_create function to create a real thread, so that the operating system will help us create the context (including initializing CPU registers and program stack). Then when the thread starts, use the C language setjmp to back up these registers to the external buffer. After creation, the thread loses its existence value, so it can be killed decisively. However, it should be noted that before creating a thread, you need to call the pthread_attr_setstack function to explicitly declare the program stack used, so that when the thread exits, the system will not automatically destroy the program stack. As for the context recovery, obviously the longjmp function is used.

Three, create a context

Below is the definition of RoutineInfo. For the sake of simplicity, all error handling codes have been omitted. The original code is in the coroutine.cpp file, and the omitted code is in the coroutine_demonstration.cpp file.

typedef void * (*RoutineHandler)(void*);

struct RoutineInfo{
    
    
	void * param;
	RoutineHandler handler;
	void * ret;
	bool stopped;

	jmp_buf buf;
	
	void *stackbase;
	size_t stacksize;
	
	pthread_attr_t attr;
	
	// size: the stack size
	RoutineInfo(size_t size){
    
    
		param = NULL;
		handler = NULL;
		ret = NULL;
		stopped = false;

		stackbase = malloc(size);
		stacksize = size;

		pthread_attr_init(&attr);
		if(stacksize)
			pthread_attr_setstack(&attr,stackbase,stacksize);
	}
	
	~RoutineInfo(){
    
    
		pthread_attr_destroy(&attr);
		free(stackbase);
	}
};

Then, we need a global list to save these RoutineInfo objects.

std::list<RoutineInfo*> InitRoutines(){
    
    
	std::list<RoutineInfo*> list;
	RoutineInfo *main = new RoutineInfo(0);
	list.push_back(main);
	return list;
}
std::list<RoutineInfo*> routines = InitRoutines();

Next is the creation of the coroutine. Note that when the coroutine is in progress, the program stack may have been damaged, so a stackBack is needed as a backup of the program stack for subsequent restoration.

void *stackBackup = NULL;
void *CoroutineStart(void *pRoutineInfo);

int CreateCoroutine(RoutineHandler handler,void* param ){
    
    
	RoutineInfo* info = new RoutineInfo(PTHREAD_STACK_MIN+ 0x4000);

	info->param = param;
	info->handler = handler;

	pthread_t thread;
	int ret = pthread_create( &thread, &(info->attr), CoroutineStart, info);

	void* status;
	pthread_join(thread,&status);

	memcpy(info->stackbase,stackBackup,info->stacksize); 	// restore the stack

	routines.push_back(info); 	// add the routine to the end of the list
	
	return 0;
}

Then there is the CoroutinneStart function. When the thread enters this function, use setjmp to save the context, then back up its own program stack, and then exit the thread directly.

void Switch();

void *CoroutineStart(void *pRoutineInfo){
    
    

	RoutineInfo& info = *(RoutineInfo*)pRoutineInfo;

	if( !setjmp(info.buf)){
    
    	
		// back up the stack, and then exit
		stackBackup = realloc(stackBackup,info.stacksize);
		memcpy(stackBackup,info.stackbase, info.stacksize);

		pthread_exit(NULL);

		return (void*)0;
	}

	info.ret = info.handler(info.param);
	
	info.stopped = true;
	Switch(); // never return
	
	return (void*)0; // suppress compiler warning
}

Four, context switching

A coroutine actively calls the Switch() function before switching to another coroutine.

std::list<RoutineInfo*> stoppedRoutines = std::list<RoutineInfo*>();

void Switch(){
    
    
	RoutineInfo* current = routines.front();
	routines.pop_front();
	
	if(current->stopped){
    
    
		// The stack is stored in the RoutineInfo object, 
		// delete the object later, now know
		stoppedRoutines.push_back(current);
		longjmp( (*routines.begin())->buf ,1);
	}
	
	routines.push_back(current);		// adjust the routines to the end of list
	
	if( !setjmp(current->buf) ){
    
    
		longjmp( (*routines.begin())->buf ,1);
	}
	
	if(stoppedRoutines.size()){
    
    
		delete stoppedRoutines.front();
		stoppedRoutines.pop_front();
	}
}

Five, demo

The user's code is very simple, just like using a thread library, a coroutine actively calls the Switch() function to actively give up CPU time to another coroutine.

#include <iostream>
using namespace std;

#include <sys/wait.h>

void* foo(void*){
    
    
	for(int i=0; i<2; ++i){
    
    
		cout<<"foo: "<<i<<endl;
		sleep(1);
		Switch();
	}
}

int main(){
    
    
	CreateCoroutine(foo,NULL);
	for(int i=0; i<6; ++i){
    
    
		cout<<"main: "<<i<<endl;
		sleep(1);
		Switch();
	}
}

Remember to add -lpthread link option when linking. The execution result of the program is as follows:

[roxma@VM_6_207_centos coroutine]$ g++ coroutime_demonstration.cpp -lpthread -o a.out
[roxma@VM_6_207_centos coroutine]$ ls
a.out  coroutime.cpp  coroutime_demonstration.cpp  README.md
[roxma@VM_6_207_centos coroutine]$ ./a.out
main: 0
foo: 0
main: 1
foo: 1
main: 2
main: 3
main: 4
main: 5

Guess you like

Origin blog.csdn.net/m0_50662680/article/details/110563509