C++中协程——State Threads

一、协程简介

1、简介

       协程(coroutine)不是进程或线程,其执行过程更类似于子例程,或者说不带返回值的函数调用。一个程序可以包含多个协程,可以对比与一个进程包含多个线程。我们知道多个线程相对独立,有自己的上下文,切换受系统控制。而协程也相对独立,有自己的上下文,但是其切换由自己控制,当前协程切换到其他协程由当前协程来控制。

       协程调度单位减小到函数,上下文切换不需要内核参与,不存在系统调用。上下文切换开销降到最低,系统调用降到最低,没有锁竞争,没有信号处理。保留了程序对请求的线性处理逻辑,提高了程序的开发效率,可扩展性和可维护性。协程是一种程序组件。通常我们把协程理解为是一种程序自己实现调度、用于提高运行效率、降低开发复杂度的东西。而State-Thread(以下简称st),就是一个由C语言编写的小巧、简洁却高效的开源协程库。这个库基于单线程运作、不强制占用用户线程,给予了开发者最大程度的轻量级和较低的侵入性。

       很多语言都拥有协程,例如python或者golang。而对于c/c++而言,通常实现协程的常见方式,通常是依赖于glibc提供的setjump&longjump或者基于汇编语言,当然还有基于语义实现(protothread)。linux上使用协程库的方式,通常也会分为替换函数和更为暴力的替换so来实现。当然而各种方式有各自的优劣。而st选用的汇编语言实现setjump&longjump和要求用户调用st_打头的函数来嵌入程序。所以st具备了跨平台的能力,以及让开发者们更开心的“与允许调用者自行选择切换时机”的能力。随着coroutine ts正式进入c++20,c++已经进入协程时代了。但是不幸的是c++20的协程标准只包含编译器需要实现的底层功能,并没有包含简单方便地使用协程的高级库,相关的类和函数进入std标准库估计要等到c++23。本文先以协程库(State Threads)来讲解c++中的协程。

2、state threads优缺点

优点:

  •        允许设计快速且高效可扩展的应用程序。应用程序将在CPU的负载和数量上都能很好的扩展。
  •        简化了应用程序的编程和调试,因为通常不需要互斥锁定,并且整个应用程序都可以自由适应静态变量和不可重入的库函数。

缺点:

  •        套接字上的所有I/O操作都必须适应st库的I/O函数,因为只有那些函数才能执行线程调度并阻止应用程序的进程被阻塞。

3、state threads工作流程

       在宏观的来看,ST的结构主要分成:

  • vp_schedule。主要是负责了一个调度的能力。有点类似于linux内核当中的schedule()函数。每次当这个函数被调用的时候,都会完成一次线程的切换。
  • 各种Queue。用于保存各种状态下等待被调度协程(st_thread)
  • Timer。用于记录各种超时和sleep。
  • poll。用于监听各种io事件,会根据系统能力不同而进行切换(kqueue、epoll、poll、select)。
  • st_thread。用于保存各种协程的信息。

其中比较重要的是schedule模块和thread模块两者。这两者实现了一个完整的协程切换和调度。属于st的核心。而schedule部分通常是开发者们最需要关心的部分。

 

二、具体实现

0、库的下载

https://sourceforge.net/projects/state-threads/

解压:tar zxvf st-1.9.tar.gz

编译:make linux-debug

解压并编译后的结果如下:

(st-1.9)解压后文件简介:

(1)doc目录

  • st.html——ST库概论
  • timeout_heap.txt——超时heap实现
  • notes.html——给出了编程注意点,包括移植,信号,进程内同步,进程间同步,非网络IO,超时处理,特别谈到进程内同步非常简单,不需要同步资源;非网络IO中谈到drawback和设计时需要避免的方法
  • reference.html——一个API接口文档介绍

(2)example目录

  • server:包含server.c和error.c。server接受一个客户端连接,接收客户端数据并返给客户端一个简单的HTML网页
  • lookupdns:包含lookupdns.c和res.c。
  • proxy:包含proxy.c。

st程序结构:

       st底层基于event-driven select/poll/kqueue/epoll等IO复用机制。下面以epoll为例说明st底层事件管理机制。

       st中有IOQ,ZOMBIEQ,RUNQ,SLEEPQ等几个队列,用来存储处于对应状态的threads。

  • RUNQ中存储的是可以被调度运行的threads,每次调用_st_vp_schedule即从该队列取出一个thread去运行。
  • IOQ存储处于IO等待状态的threads,当上层调用st_poll时,将该thread放入IOQ中;当底层epoll有IO事件到达时,将该thread从IOQ中移除,并放入RUNQ中。
  • 当thread退出时,放入ZOMBIEQ中。
  • 当st_poll传入超时参数>0或调用st_usleep和st_cond_timewait时,将thread加入SLEEPQ中。

setjmp和longjmp:

(1)setjmp

#include <setjmp.h>
int setjmp(jmp_buf env);

       该函数将栈的上下/环境保存在env中,供longjmp使用。

       返回值:如果直接返回,则返回0;如果使用保存的上下文从longjmp(3)返回,则返回非零。           

(2)longjmp

#include <setjmp.h>
void longjmp(jmp_buf env, int val);

       使用相应的env参数还原最后一次调用setjmp(3)保存的env。由此可知:

setjmp第一次返回0,第二次返回longjmp中的val参数。

调用longjmp后,返回setjmp函数的返回地址处。

 

基于setjmp和longjmp实现协程库基本步骤(下述线程指用户线程):

  • (1)需要用jmpbuf变量保存每一个线程的运行时环境,称为线程上下文context。
  • (2)为每个线程分配(malloc/mmap)一个stack,用于该线程运行时栈,该stack完全等效于普通系统线程的函数调用栈。该stack地址是在线程初始化时设置,所以不需要考虑setjmp时保存线程的栈上frames数据的问题。
  • (3)通过调用setjmp初始化线程运行时上下文,将context数据存放到jmpbuf结构中。然后修改其中的栈指针sp指向上一步分配的stack。根据当前系统栈的增长方向,将sp设置为stack的最低或最高地址。
  • (4)线程退出时,需要返回到一个安全的系统位置。即,需要有一个主线程main thread或idle thread来作为其他线程最终的退出跳转地址。需要为主线程保存一个jmpbuf。
  • (5)设置过main thread的jmpbuf后,需要跳转到其他线程开始执行业务线程。
  • (6)实现一个context交换函数,在多个线程之间进行跳转:保存自己的jmpbuf,longjmp到另一个线程的jmpbuf。

例:

#include <stdio.h>
#include <setjmp.h>

jmp_buf buf;
int times = 0;

void second(int *k)
{
	printf("second times = %d\n", ++(*k));
	longjmp(buf, 65536);
}

void first(int *k)
{
	printf("first times = %d\n", ++(*k));
	second(k);
}

int main()
{
	int ret = setjmp(buf);
	if (0 == ret){
		printf("1. ret is %d\n", ret);
		first(&times);
	} else{
		printf("2. ret is %d\n", ret);
	}

	return 0;
}

输出:

2、线程初始化

#define MD_INIT_CONTEXT(_thread, _sp, _main) \
  ST_BEGIN_MACRO                             \
  if (MD_SETJMP((_thread)->context))         \
    _main();                                 \
  MD_GET_SP(_thread) = (long) (_sp);         \
  ST_END_MACRO

       setjmp(将jmpbuf存放到thread->context)之后,同时修改它的栈指针sp指向新分配的线程stack->sp地址。该sp指针用于该thread以后的栈frames数据存储。

3、线程切换

#define _ST_SWITCH_CONTEXT(_thread)       \
    ST_BEGIN_MACRO                        \
    ST_SWITCH_OUT_CB(_thread);            \
    if (!MD_SETJMP((_thread)->context)) { \
      _st_vp_schedule();                  \
    }                                     \
    ST_DEBUG_ITERATE_THREADS();           \
    ST_SWITCH_IN_CB(_thread);             \
    ST_END_MACRO

       其中主要时MD_SETJMP保存当前context,然后调用_st_vp_schedule()从_ST_RUNQ上取第一个可运行的thread,并调用_ST_RESTORE_CONTEXT将该thread恢复运行。

4、线程恢复

#define _ST_RESTORE_CONTEXT(_thread)   \
    ST_BEGIN_MACRO                     \
    _ST_SET_CURRENT_THREAD(_thread);   \
    MD_LONGJMP((_thread)->context, 1); \
    ST_END_MACRO

5、起始线程primordial thread和休眠线程idle thread

/*
 * Initialize this Virtual Processor
 */
int st_init(void)
{
  _st_thread_t *thread;

  if (_st_active_count) {
    /* Already initialized */
    return 0;
  }

  /* We can ignore return value here */
  st_set_eventsys(ST_EVENTSYS_DEFAULT);

  if (_st_io_init() < 0)
    return -1;

  memset(&_st_this_vp, 0, sizeof(_st_vp_t));

  ST_INIT_CLIST(&_ST_RUNQ);
  ST_INIT_CLIST(&_ST_IOQ);
  ST_INIT_CLIST(&_ST_ZOMBIEQ);
#ifdef DEBUG
  ST_INIT_CLIST(&_ST_THREADQ);
#endif

  if ((*_st_eventsys->init)() < 0)
    return -1;

  _st_this_vp.pagesize = getpagesize();
  _st_this_vp.last_clock = st_utime();

  /*
   * Create idle thread
   */
  _st_this_vp.idle_thread = st_thread_create(_st_idle_thread_start,
                         NULL, 0, 0);
  if (!_st_this_vp.idle_thread)
    return -1;
  _st_this_vp.idle_thread->flags = _ST_FL_IDLE_THREAD;
  _st_active_count--;
  _ST_DEL_RUNQ(_st_this_vp.idle_thread);

  /*
   * Initialize primordial thread
   */
  thread = (_st_thread_t *) calloc(1, sizeof(_st_thread_t) +
                   (ST_KEYS_MAX * sizeof(void *)));
  if (!thread)
    return -1;
  thread->private_data = (void **) (thread + 1);
  thread->state = _ST_ST_RUNNING;
  thread->flags = _ST_FL_PRIMORDIAL;
  _ST_SET_CURRENT_THREAD(thread);
  _st_active_count++;
#ifdef DEBUG
  _ST_ADD_THREADQ(thread);
#endif

  return 0;
}

       在st_init里面,创建primordial thread和idle thread。primordial thread作为起始线程当有其他线程加入运行队列后从该线程切出,idle thread作为背景线程在没有可运行线程的时候执行io调度函数分发事件。

       st_init里面调用st_thread_create并不会开始执行idle线程,创建其他线程也一样,只有在直接或间接调用_st_vp_schedule之后才会开始执行RUNQ上面的线程。_st_vp_schedule函数在_ST_SWITCH_CONTEXT中被调用。

6、事件系统(_st_eventsys_t)

常见的方式:poll/select/epoll/kqueue等等(默认采用select)。

typedef struct _st_eventsys_ops {
 const char *    name;     /* Name of this event system */
 int    val;               /* Type of this event system */
 int    (*init)(void);     /* Initialization */
 void   (*dispatch)(void); /* Dispatch function */
 int    (*pollset_add)(struct pollfd *, int);     /* Add descriptor set */
 void   (*pollset_del)(struct pollfd *, int);    /* Delete descriptor set */
 int    (*fd_new)(int);       /* New descriptor allocated */
 int    (*fd_close)(int);     /* Descriptor closed */
 int    (*fd_getlimit)(void); /* Descriptor hard limit */
} _st_eventsys_t;

7、协程态(_st_vp_t)

每个vp对应一个idle协程,4个队列,切换回调等。

typedef struct _st_vp {
  _st_thread_t *idle_thread;  /* Idle thread for this vp */
  st_utime_t last_clock;      /* The last time we went into vp_check_clock() */

  _st_clist_t run_q;          /* run queue for this vp */
  _st_clist_t io_q;           /* io queue for this vp */
  _st_clist_t zombie_q;       /* zombie queue for this vp */
#ifdef DEBUG
  _st_clist_t thread_q;       /* all threads of this vp */
#endif
  int pagesize;

  _st_thread_t *sleep_q;      /* sleep queue for this vp */
  int sleepq_size;          /* number of threads on sleep queue */

#ifdef ST_SWITCH_CB
  st_switch_cb_t switch_out_cb;    /* called when a thread is switched out */
  st_switch_cb_t switch_in_cb;    /* called when a thread is switched in */
#endif
} _st_vp_t;

8、队列(run/io/zombie/sleep queue)

       st使用了4个队列, 这个在上面的结构中就已经有,为了访问的方便,定义了4组宏(debug模式下,还有另一个),如下。这4个队列分别对应了4种状态,由此形成自己的状态机体系。

       每当创建一个微线程时,都会将其加入到RUNQ中。

#define _ST_RUNQ    (_st_this_vp.run_q)
#define _ST_IOQ     (_st_this_vp.io_q)
#define _ST_ZOMBIEQ (_st_this_vp.zombie_q)
#ifdef DEBUG
#define _ST_THREADQ (_st_this_vp.thread_q)
#endif
#define _ST_SLEEPQ  (_st_this_vp.sleep_q)

9、微线程(_st_thread_t)

       在vp中,我们看到有一个成员:idle_thread. 自然会有疑问:这货是干嘛的?这货长的像个线程,但其实当然不是线程,就以“微线程”来称吧:即用户态下实现的线程。

主要关注点:

  • 微线程的状态机
  • 每个微线程维护的栈空间、私有数据
  • 每个微线程的入口点
  • 我们熟悉的jmp_buf上下文:context
typedef struct _st_thread _st_thread_t;

struct _st_thread {
 int     state; /* Thread's state */
 int     flags; /* Thread's flags */

 void *   (*start)(void *arg); /* The start function of the thread */
 void *   arg;                 /* Argument of the start function */
 void *   retval;             /* Return value of the start function */

 _st_stack_t *   stack;       /* Info about thread's stack */

 _st_clist_t     links;       /* For putting on run/sleep/zombie queue */
 _st_clist_t    wait_links;  /* For putting on mutex/condvar wait queue */
#ifdef DEBUG
 _st_clist_t    tlink;       /* For putting on thread queue */
#endif

 st_utime_t     due;      /* Wakeup time when thread is sleeping */
 _st_thread_t * left;     /* For putting in timeout heap */
 _st_thread_t * right;    /* -- see docs/timeout_heap.txt for details */
 int            heap_index;

 void **         private_data; /* Per thread private data */

 _st_cond_t *    term;       /* Termination condition variable for join */

 jmp_buf         context;    /* Thread's context */
};

10、栈(_st_stack_t)

       每个微线程自身单独都会维护自己的一个“栈空间”。栈有自己独立的内存,大小,栈底,栈顶,栈指针(程序级别、非系统内存级别),恢复点。这里的“栈”只是一个概念(实际是堆,通过malloc或mmap来实现),并非我们通常指的栈。

typedef struct _st_stack {
    _st_clist_t     links;
    char *          vaddr;              /* Base of stack's allocated memory */
    int             vaddr_size;         /* Size of stack's allocated memory */
    int             stk_size;           /* Size of usable portion of the stack */
    char *          stk_bottom;         /* Lowest address of stack's usable portion */
    char *          stk_top;            /* Highest address of stack's usable portion */
    void *          sp;                 /* Stack pointer from C's point of view */
#ifdef __ia64__
    void *          bsp;                /* Register stack backing store pointer */
#endif
} _st_stack_t;

       _st_stack的可用空间大小缺省为:128k,即刚开始分配时的大小。当不够用时,会增加一个内存页的大小(getpagesize),当然的大小必须是内存页的整数倍。(详见st_thread_create)。这部分是可用栈空间的大小,实际大小还会加上:2*REDZONE+extra。 在stack的每一端(栈顶、栈底)都有一个REDZONE,其大小也是一个分页;而extra则看是否需要(0或者一个分页大小)

       在st_stack中,入栈的方式,有两种:向下增长,向上增加。以向上增长为例(向下增长的话,类似,反之即可)

11、jmp_buf上下文

       即然栈是自己实现的,那边对应的jmp_buf里头的sp、bsp也需要跟着变。

...
_ST_INIT_CONTEXT(thread, stack->sp, stack->bsp, _st_thread_main);
...
#define MD_SETJMP(env)           _setjmp(env)
#define MD_LONGJMP(env, val)     _longjmp(env, val)
...
#define MD_INIT_CONTEXT(_thread, _sp, _bsp, _main) \
ST_BEGIN_MACRO \
if (MD_SETJMP((_thread)->context)) \
    _main(); \
memcpy((char *)(_bsp) - MD_STACK_PAD_SIZE, \
        (char *)(_thread)->context[0].__jmpbuf[17] - MD_STACK_PAD_SIZE, \
        MD_STACK_PAD_SIZE); \
(_thread)->context[0].__jmpbuf[0] = (long) (_sp); \
(_thread)->context[0].__jmpbuf[17] = (long) (_bsp); \
ST_END_MACRO

 

三、微线程的创建

1、整体流程

       微线程的创立,通过_st_thread_create()来完成。

主要过程如下:

  • (1)创建栈,分配空间。
  • (2)初始化sp/bsp、私有数据(private_data)以及微线程自身所需空间(thread),入口函数(start)及参数。
  • (3)调用setjmp,若成功,则执行_st_thread_main();
  • (4)将之前的sp恢复.

       其中的第三步,_st_thread_main,也就是微线程自身的执行点,过程也很简单:

  • (1)获取当前微线程
  • (2)将微线程对应的栈空间位置状态变量置为0(state和flags,转型为valatile,防止longjmp切换时更改值。ps: 这里有个关于二级指针数组[1]的tricks)
  • (3)执行微线程的入口函数(start)
  • (4)退出微线程(_st_thread_exit)

      退出微线程时,会发生一些很奇妙的事情:

  • (1)清理工作
  • (2)对term做检查,是否已退出。若是,添加thread到僵尸队列:zombieq中, 通知term信号,并切换context(_ST_SWITCH_CONTEXT,清理term
  • (3)清理stack(_st_stack_free)
  • (4)切换至另一个thread执行(_ST_SWITCH_CONTEXT)

       上面就是一个整体的流程。而其中有两个地方,需要特别再关注一下。

2、上下文切换

       因为涉及到微线程的切换,即然是“切换”,那么这里肯定至少涉及到两个微线程。由于微线程本身处在RUNQ中,切换的话,自然会将next。

  • (1)如果runq队列有在运行,切换至next; 如果没有,切换至idle_thread
  • (2)将切换后的当前微线程置为run状态
  • (3)longjmp置相应微线程的jmp_buf处,恢复栈执行

 

四、实例

#include <stdio.h>
#include <stdlib.h>
#include "st.h"

using namespace std;

void* do_calc(void* arg) {
	int sleep_ms = (int)(long int)(char*)arg * 10;

	for (;;) {
		printf("in sthread #%dms\n", sleep_ms);
		st_usleep(sleep_ms * 1000);
	}

	return NULL;
}

int main(int argc, char** argv) {
	if (argc <= 1) {
		printf("Test the concurrence of state-threads!\n"
			"Usage: %s <sthread_count>\n"
			"eg. %s 10000\n", argv[0], argv[0]);
		return -1;
	}

	if (st_init() < 0) {
		printf("error!");
		return -1;
	}

	int i;
	int count = atoi(argv[1]);
	for (i = 1; i <= count; i++) {
		if (st_thread_create(do_calc, (void*)i, 0, 0) == NULL) {
			printf("error!");
			return -1;
		}
	}

	st_thread_exit(NULL);

	return 0;
}

输出参数5时,输出如下:

 

参考:

http://state-threads.sourceforge.net/ :官网

http://state-threads.sourceforge.net/docs/st.html :state thread官网介绍

https://blog.csdn.net/win_lin/article/details/8242653 :译文

http://ossrs.github.io/state-threads/docs/reference.html#set_eventsys :api文档

https://blog.csdn.net/tao_627/article/details/45788013:state thread库目录介绍

猜你喜欢

转载自blog.csdn.net/King_weng/article/details/113184401