• 协程库st(state threads library)原理解析


    协程库state threads library(以下简称st)是一个基于setjmp/longjmp实现的C语言版用户线程库或协程库(user level thread)。

    这里有一个基本的协程例子 http://www.csl.mtu.edu/cs4411.ck/www/NOTES/non-local-goto/coroutine.html, 可以了解setjmp和longjmp的基本用法。如还有不懂,请自行查阅其他资料。本文主要关注st基于setjmp和longjmp的实现原理及其程序结构。

    st基本介绍 http://state-threads.sourceforge.net/docs/st.html

    从中可以看出,IA(Internet Application)架构演化历史:

    1.多进程MP

    以Apache为代表的web server。创建进程服务新用户,开销过高。调度单位是进程。

    2.多线程MT

    创建新线程服务新用户,线程间上下文切换,锁竞争等等,增加了额外开销。调度单位是线程。

    3.事件驱动状态机EDSM

    基于IO复用机制,实现大量并发请求的处理。但程序是一体的,并不是基于线程,新程序需要从头开始。该模式下主要使用回调和状态参数来进行上下文切换,实际上是以一种非常艰难和痛苦的方式实现了类似线程和栈的思想。ESDM最大的问题是其“将线性思路分解成大量的回调所固有的复杂性”,导致程序难以实现,扩展和维护。

    传统EDSM程序架构:

    4.协程

    调度单位减小到函数,上下文切换不需要内核参与,不存在系统调用。上下文切换开销降到最低,系统调用降到最低,没有锁竞争,没有信号处理。保留了程序对请求的线性处理逻辑,提高了程序的开发效率,可扩展性和可维护性。

    基于st的ESDM程序模型:

    基于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。

    st基于setjmp和longjmp的具体实现:

    线程初始化:

    复制代码
    #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数据存储。

    线程切换:

    复制代码
    #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恢复运行。

    线程恢复:

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

    起始线程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中被调用。

    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中。
  • 相关阅读:
    非阻塞式NIO 小案例(模拟聊天室)
    网络通信小案例,服务端接收成功要给客户端一个反馈(阻塞式)
    阻塞式网络通信小案例:
    NIO的非阻塞式网络通信
    字符编码
    使用分散(Scatter)与聚集(Gather)来实现文件的复制
    使用通道之间的数据传输(效果,也是实现文件的复制)
    创建直接缓存区完成文件的复制
    C++预处理详解
    C++的学习资源
  • 原文地址:https://www.cnblogs.com/lidabo/p/14439617.html
Copyright © 2020-2023  润新知