Erlo

开源的 c 语言网络协程库 state_thread 源码分析

时间:2021-04-20   阅读:63次   来源:开源中国
页面报错
点赞

state thread是一个开源的c语言网络协程库,它在用户空间实现了协程调度
st最初是由网景(Netscape)公司的MSPRNetscape Portable Runtime library)项目中剥离出来,后由SGISilicon Graphic Inc)和Yahoo!公司(前者是主力)共同开发维护。
2001年发布v1.0以来一直到2009v1.9稳定版后未再变动

State Threads:回调终结者(必读)
https://blog.csdn.net/caoshangpa/article/details/79565411

st-1.9.tar.gz 是原版, http://state-threads.sourceforge.net/
state-threads-1.9.1.tar.gz srs修改版, https://github.com/ossrs/state-threads

st源码编译
tar zxvf st-1.9.tar.gz

cd st-1.9
make linux-debug          // make命令可以查看支持的编译选项
obj目录有编译生成的文件st.h, lib*.solib*.a

examples目录有几个例子lookupdnsproxyserver

需要的知识点
汇编语言(非必需)
线程的栈管理(非必需)
线程的调度和同步(必须)线程不同步的测试代码thread.c
4 setjmp/longjmp的使用(必须)。测试代码setjmp.c
5 epoll原理和使用(必须)。测试代码epoll_server.c 和 epoll_client.c

分析state_thread源码的目的,是为了正确的使用它
st中thread其实是协程的概念
st_xxx分为 io  延迟类

一些重要的数据结构
_st_vp_t _st_this_vp;     virtual processor 虚拟处理器
_st_thread_t *_st_this_thread;
_st_clist_t  run_q, io_q, zombie_q, thread_q 
_st_thread_t  *idle_thread, *sleep_q

代码分析
st库自带的example业务逻辑较为复杂,有兴趣可以看下。
为了简化问题,编写了测试代码st-1.9/examples/st_epoll.c,依据此代码提出问题分析问题。
st_init()做了什么?
_st_idle_thread_start()做了什么
st_thread_create()做了什么?
st_thread_exit()做了什么?
st_usleep()做了什么?
主业务逻辑(无限循环)协程是如何调度的?
监听的文件描述符是如何调度的?
协程如何正常退出?
1 没有设置终止条件变量(不可以被join)的协程直接return即可退出; 
2 设置了终止条件变量(可以被join)的协程退出时,先把自己加入到zombie_q中,然后通知等待的协程,等待的协程退出后,自己在退出。

协程的join(连接)是什么意思?

1 创建协程a的时候 st_thread_create(handle_cycle, NULL, 1, 0) 要设置为1, 表示该协程可以被join

2 协程b代码里要掉用st_thread_join(thread, retvalp),表示我要join到协程a上

3 join的意思是 协程a和协程b 有一定关联行,在协程退出时,要先退出协程b 才能退出协程a

4 st中一个协程只能被另一个协程join,不能被多个协程join

5 可以被join的协程a,在没有其他协程join时,协程a无法正常退出

st里的mutex有什么用?

通常情况下st的多协程是不需要加锁的,但是在有些情况下需要锁来保证原子操作,下面会详细说明。

st_mutex_new(void); 创建锁

st_mutex_destroy(st_mutex_t lock); 等待队列必须为空才能销毁锁

st_mutex_lock(st_mutex_t lock); 第一次掉用能获得锁,以后掉用会加入锁的等待队列中(FIFO)

st_mutex_unlock(st_mutex_t lock); 释放锁并激活等待队列的协程

st_mutex_trylock(st_mutex_t lock); 尝试获得锁不会加入到等待队列

st里的cond有什么用?

通常情况下st的多协程是不需要条件变量的,但是有些情况下需要条件变量来保证协程执行的先后顺序,比如:协程a要先于协程b执行

st_cond_new(void); 创建条件变量

st_cond_destroy(st_cond_t cvar); 等待队列必须为空才能销毁条件变量

st_cond_timedwait(st_cond_t cvar, st_utime_t timeout); 限时等待条件变量,会加入条件变量的等待队列中(FIFO),并加入到sleep_q队列中(可能先于FIFO的顺序被调度到)

st_cond_wait(st_cond_t cvar); 阻塞等待条件变量,会加入条件变量的等待队列中(FIFO)

st_cond_signal(st_cond_t cvar); 唤醒阻塞在条件变量上的一个协程

st_cond_broadcast(st_cond_t cvar); 唤醒阻塞在条件变量上的全部协程

 

 

st中与调度有关的函数

st的setjmp

#define _ST_SWITCH_CONTEXT(_thread)                   协程切换的两个宏函数之一,停止当前协程并运行其他协程

  ST_BEGIN_MACRO                      

  ST_SWITCH_OUT_CB(_thread);                                 协程切走时调用的函数,一般不管用

  if (!MD_SETJMP((_thread)->context))                          汇编语言实现 应该跟setjmp()一样 首次掉用返回0

  {                                   

    _st_vp_schedule();                                                    核心调度函数

  }                                   

  ST_DEBUG_ITERATE_THREADS();         

  ST_SWITCH_IN_CB(_thread);                                     协程切回时调用的函数,一般不管用

  ST_END_MACRO

 

st的longjmp

#define _ST_RESTORE_CONTEXT(_thread)                协程切换的两个宏函数之一,恢复线程运行

  ST_BEGIN_MACRO                     

  _ST_SET_CURRENT_THREAD(_thread);                   设置全局变量 _st_this_thread = _thread

  MD_LONGJMP((_thread)->context, 1);                        汇编语言实现 应该跟longjmp()一样返回值永远为1

  ST_END_MACRO

 

MD_SETJMP的时候,会使用汇编把所有寄存器的信息保留下来,而MD_LONGJMP则会把所有的寄存器信息重新加载出来。两者配合使用的时候,可以完成函数间的跳转。

 

st的核心调度函数

void _st_vp_schedule(void)

{

  _st_thread_t *thread;

 

  printf("in _st_vp_schedulen");

  printf("_st_active_count = %dn", _st_active_count);

  if (_ST_RUNQ.next != &_ST_RUNQ)

  {

    printf("use runqn");

    /* Pull thread off of the run queue */

    thread = _ST_THREAD_PTR(_ST_RUNQ.next);

    _ST_DEL_RUNQ(thread);

  }

  else

  {

    printf("use idlen");

    /* If there are no threads to run, switch to the idle thread */

    thread = _st_this_vp.idle_thread;

  }

  ST_ASSERT(thread->state == _ST_ST_RUNNABLE);

 

  /* Resume the thread */

  thread->state = _ST_ST_RUNNING;

  _ST_RESTORE_CONTEXT(thread);

}

 

st辅助调度函数

void *_st_idle_thread_start(void *arg)

{

  printf("i'm in _st_idle_thread_start()n");

  _st_thread_t *me = _ST_CURRENT_THREAD();

 

  while (_st_active_count > 0)

  {

    /* Idle vp till I/O is ready or the smallest timeout expired */

    printf("call _st_epoll_dispatch()n");

    _ST_VP_IDLE();                                                       处理io类事件

 

    /* Check sleep queue for expired threads */

    _st_vp_check_clock();                                               处理延时类事件

 

    me->state = _ST_ST_RUNNABLE;

    _ST_SWITCH_CONTEXT(me);                                  从这里恢复运行,然后判断_st_active_count的值

  }

 

  /* No more threads */

  exit(0);                                                                        整个程序退出

 

  /* NOTREACHED */

  return NULL;

}

 

会触发协程切换的函数有哪些?

sched.c:86: _ST_SWITCH_CONTEXT(me); 59 int st_poll(struct pollfd *pds, int npds, st_utime_t timeout)

sched.c:234: _ST_SWITCH_CONTEXT(me); 221 void *_st_idle_thread_start(void *arg)

sched.c:261: _ST_SWITCH_CONTEXT(thread); 244 void st_thread_exit(void *retval)

sched.c:276: _ST_SWITCH_CONTEXT(thread); 244 void st_thread_exit(void *retval)

sync.c:131: _ST_SWITCH_CONTEXT(me); 115 int st_usleep(st_utime_t usecs)

sync.c:198: _ST_SWITCH_CONTEXT(me); 180 int st_cond_timedwait(_st_cond_t *cvar, st_utime_t timeout)

sync.c:315: _ST_SWITCH_CONTEXT(me); 290 int st_mutex_lock(_st_mutex_t *lock)

 

sched.c:134: _ST_RESTORE_CONTEXT(thread); 115 void _st_vp_schedule(void)

 

st的优缺点

优点:

1 用户空间实现协程调度,降低了用户空间和内核空间的切换,一定程度上提高了程序效率。

2 由于是在单核上的单线程多协程,同一时间只会有一个协程在运行,所以对于全局变量也不需要做协程同步。

   共享资源释放函数只需做到可重入就行,所谓的可重入就是释放之前先判断是否为空值,释放后要赋空值。

3 协程使用完,直接return即可,st会回收协程资源并做协程切换。

可以通过向run_q链表头部加入协程,来实现优先调度。

5 st支持多个操作系统,比如 AIX,CYGWIN,DARWIN,FREEBSD,HPUX,IRIX,LINUX,NETBSD,OPENBSD,SOLARIS

缺点:

所有I/O操作必须使用st提供的API,只有这样协程才能被调度器管理。

2 所有协程里不能使用sleep(),sleep()会造成整个线程sleep。

被调度到的协程不会限制运行时长,如果有协程cpu密集型或死循环,就会严重阻碍其他协程运

4 单进程单线程,只能使用单核,想要通过多个cpu提高并发能力,只能开多个程序(进程),多进程通信较麻烦。

 

补充知识点

线程为什么要同步?

线程由内核自动调度

同一个进程上的线程共享该进程的整个虚拟地址空间

同一个进程上的线程代码区是共享的,即不同的线程可以执行同样的函数

所以在并发环境中,多个线程同时对同一个内存地址进行写入,由于CPU寄存器时间调度上的问题,写入数据会被多次的覆盖,会造成共享数据损坏,所以就要使线程同步。

 

2 什么情况下需要线程同步?

评论留言

还没有评论留言,赶紧来抢楼吧~~

吐槽小黑屋()

* 这里是“吐槽小黑屋”,所有人可看,只保留当天信息。

  • Erlo吐槽

    Erlo.vip2021-08-05 00:28:06Hello、欢迎使用吐槽小黑屋,这就是个吐槽的地方。
  • 返回顶部

      给这篇文章打个标签吧~

      棒极了 糟糕透顶 好文章 PHP JAVA JS 小程序 Python SEO MySql 确认