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

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

这里有一个基本的协程例子 Building Coroutines, 可以了解setjmp和longjmp的基本用法。如还有不懂,请自行查阅其他资料。本文主要关注st基于setjmp和longjmp的实现原理及其程序结构。

st基本介绍 State Threads for Internet Applications

从中可以看出,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中。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: "Error" 是一个英文词汇,意为“错误”或“出错”。在计算机科学领域中,当程序不能成功执行任务时就会发生错误。 在软件开发中,无论是程序员还是测试人员,都会面临各种类型的错误。其中一些错误可能是语法错误,因为程序员犯了一些编码错误,导致程序无法编译或运行。而另一些错误则可能是由于硬件或软件环境的问题而导致的,如网络连接问题、外部设备故障等等。 无论错误的原因是什么,它都是非常重要的。在程序执行中,任何一个小错误都可能导致程序崩溃或无法按照预期工作。因此,在开发过程中及时使用调试工具来查找和修复错误是非常重要的。 在实际使用计算机和软件时,可能会遇到各种类型的错误。例如,输入错误的用户名或密码、无法访问网站等等。当这些问题发生时,计算机通常会显示相应的错误消息,以帮助用户识别和解决问题。 总之,“Error” 是一个非常重要的词汇,在计算机科学和软件开发中非常常见,它提示我们需要调查和解决问题,以确保计算机和软件能够正常运行。 ### 回答2: 错误:是指在计算机程序或操作中出现的不符合预期或未经验证的行为。在计算机编程中,出现错误可能会导致程序崩溃、数据丢失、系统错误等问题。例如,在编写一个程序时,程序员可能会因为语法错误、变量拼写错误、算术错误或逻辑错误而导致错误。此外,硬件故障、网络问题、不兼容的软件、安装错误以及恶意软件等情况也可能导致错误。在调试程序时,程序员通常会利用错误信息确定错误,然后尝试改正它。当然,有一些错误可能比较严重,需要更多的时间和努力来纠正。因此,在编写任何计算机程序时,始终需要遵循最佳实践,以尽可能地减少错误的数量。 ### 回答3: Error是指计算机程序或系统发生的错误或异常情况。当程序中出现错误时,计算机会自动产生一系列错误代码,用来描述错误细节。错误代码通常包含在错误信息中,以便开发者或用户更快地定位错误并进行修复。 错误通常分为逻辑错误和运行时错误。逻辑错误主要是因为程序设计不合理造成的,例如代码逻辑出错、算法错误等。这类错误通常比较难找到,需要通过代码反复检查才能找出。而运行时错误则是指在程序运行过程中出现的错误,例如空指针引用、数组越界等。这种错误通常比较容易发现,由于程序无法继续执行,在终端或日志中会直接显示错误信息。 为了避免错误的出现,我们应该在编写代码时认真思考、分析问题,严格遵守编码规范和标准,及时测试和调试程序。当程序出现错误时,我们应该尽早地对其进行修复,找到并消除错误的根源,以确保程序的正常运行。同时,在处理程序错误时,我们应该注意保护用户数据的安全,防止数据丢失或泄漏等问题的发生。 总之,错误是计算机程序或系统中不可避免的问题,我们应该采取积极有效的措施来预防和处理错误,提高程序的质量和可靠性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值