st任务状态转换
3.1 st的状态转换实现
dispatch的st的主调度模块,主要负责将io事件和超时事件转换为runnalbe task,源码:
void *_st_idle_thread_start(void *arg)
{
_st_thread_t *me = _ST_CURRENT_THREAD();
while (_st_active_count > 0) {
/* Idle vp till I/O is ready or the smallest timeout expired */
_ST_VP_IDLE();
/* Check sleep queue for expired threads */
_st_vp_check_clock();
me->state = _ST_ST_RUNNABLE;
_ST_SWITCH_CONTEXT(me);
}
/* No more threads */
exit(0);
/* NOTREACHED */
return NULL;
}
st_vp_idle是i/o事件,st可以选择select、poll、kqueue模型,本文主要介绍下epoll的实现,epoll的实现有几个重要环节:
1.timeout的获取,通过等待队列最小堆方式排序,获取堆顶元素,如果堆为空,则阻塞等待
2. wait i/o事件
3. 找到文件句柄对应的处理协程,将协程放入可执行队列内
ST_HIDDEN void _st_epoll_dispatch(void)
{
...
if (_ST_SLEEPQ == NULL) {
timeout = -1;
} else {
min_timeout = (_ST_SLEEPQ->due <= _ST_LAST_CLOCK) ? 0 :
(_ST_SLEEPQ->due - _ST_LAST_CLOCK);
timeout = (int) (min_timeout / 1000);
}
/* Check for I/O operations */
nfd = epoll_wait(_st_epoll_data->epfd, _st_epoll_data->evtlist,
_st_epoll_data->evtlist_size, timeout);
if (nfd > 0) {
for (i = 0; i < nfd; i++) {
osfd = _st_epoll_data->evtlist[i].data.fd;
_ST_EPOLL_REVENTS(osfd) = _st_epoll_data->evtlist[i].events;
if (_ST_EPOLL_REVENTS(osfd) & (EPOLLERR | EPOLLHUP)) {
/* Also set I/O bits on error */
_ST_EPOLL_REVENTS(osfd) |= _ST_EPOLL_EVENTS(osfd);
}
}
for (q = _ST_IOQ.next; q != &_ST_IOQ; q = q->next) {
pq = _ST_POLLQUEUE_PTR(q);
...
if (notify) {
ST_REMOVE_LINK(&pq->links);
pq->on_ioq = 0;
/*
* Here we will only delete/modify descriptors that
* didn't fire (see comments in _st_epoll_pollset_del()).
*/
_st_epoll_pollset_del(pq->pds, pq->npds);
if (pq->thread->flags & _ST_FL_ON_SLEEPQ)
_ST_DEL_SLEEPQ(pq->thread);
pq->thread->state = _ST_ST_RUNNABLE;
_ST_ADD_RUNQ(pq->thread);
}
}
for (i = 0; i < nfd; i++) {
/* Delete/modify descriptors that fired */
osfd = _st_epoll_data->evtlist[i].data.fd;
_ST_EPOLL_REVENTS(osfd) = 0;
events = _ST_EPOLL_EVENTS(osfd);
op = events ? EPOLL_CTL_MOD : EPOLL_CTL_DEL;
ev.events = events;
ev.data.fd = osfd;
if (epoll_ctl(_st_epoll_data->epfd, op, osfd, &ev) == 0 &&
op == EPOLL_CTL_DEL) {
_st_epoll_data->evtlist_cnt--;
}
}
}
}
注:st的epoll实现效率不高,需要遍历所有的_ST_IOQ这个实现,这个逻辑违背了epoll的实现,退化为select模型
_st_vp_check_clock的实现比较巧妙,通过最小堆形式保存超时等待的任务,最早超时的在顶部,所以每次加入和删除超时队列,只需要做对应的heap调整,避免了全部排序,当出现超时将协程状态变为超时,加入可执行队列状态。源码:
void _st_vp_check_clock(void)
{
_st_thread_t *thread;
st_utime_t elapsed, now;
now = st_utime();
elapsed = now - _ST_LAST_CLOCK;
_ST_LAST_CLOCK = now;
if (_st_curr_time && now - _st_last_tset > 999000) {
_st_curr_time = time(NULL);
_st_last_tset = now;
}
while (_ST_SLEEPQ != NULL) {
thread = _ST_SLEEPQ;
ST_ASSERT(thread->flags & _ST_FL_ON_SLEEPQ);
if (thread->due > now)
break;
_ST_DEL_SLEEPQ(thread);
/* If thread is waiting on condition variable, set the time out flag */
if (thread->state == _ST_ST_COND_WAIT)
thread->flags |= _ST_FL_TIMEDOUT;
/* Make thread runnable */
ST_ASSERT(!(thread->flags & _ST_FL_IDLE_THREAD));
thread->state = _ST_ST_RUNNABLE;
_ST_ADD_RUNQ(thread);
}
}
3.2 协程的初始化
初始化最重要的两个点
1. 协程栈的初始化;协程栈通过mmap分配
2. 设置setjimp,当dispatch调度任务后进入协程入库函数
st协程的初始化源码:
_st_thread_t *st_thread_create(void *(*start)(void *arg), void *arg, int joinable, int stk_size)
{
_st_thread_t *trd;
_st_stack_t *stack;
void **ptds;
char *sp;
/* Adjust stack size */
if (stk_size == 0) {
stk_size = ST_DEFAULT_STACK_SIZE;
}
stk_size = ((stk_size + _ST_PAGE_SIZE - 1) / _ST_PAGE_SIZE) * _ST_PAGE_SIZE;
stack = _st_stack_new(stk_size);
if (!stack) {
return NULL;
}
/* Allocate thread object and per-thread data off the stack */
sp = stack->stk_top;
/*
* The stack segment is split in the middle. The upper half is used
* as backing store for the register stack which grows upward.
* The lower half is used for the traditional memory stack which
* grows downward. Both stacks start in the middle and grow outward
* from each other.
*/
/**
The below comments is by winlin:
The Stack public structure:
+--------------------------------------------------------------+
| stack |
+--------------------------------------------------------------+
bottom top
The code bellow use the stack as:
+-----------------+-----------------+-------------+------------+
| stack of thread |pad+align(128B+) |thread(336B) | keys(128B) |
+-----------------+-----------------+-------------+------------+
bottom sp trd ptds top
(context[0].__jmpbuf.sp) (private_data)
*/
sp = sp - (ST_KEYS_MAX * sizeof(void *));
ptds = (void **) sp;
sp = sp - sizeof(_st_thread_t);
trd = (_st_thread_t *) sp;
/* Make stack 64-byte aligned */
if ((unsigned long)sp & 0x3f) {
sp = sp - ((unsigned long)sp & 0x3f);
}
stack->sp = sp - _ST_STACK_PAD_SIZE;
memset(trd, 0, sizeof(_st_thread_t));
memset(ptds, 0, ST_KEYS_MAX * sizeof(void *));
/* Initialize thread */
trd->private_data = ptds;
trd->stack = stack;
trd->start = start;
trd->arg = arg;
if (MD_SETJMP((trd)->context)) {
_st_thread_main();
}
MD_GET_SP(trd) = (long) (stack->sp);
/* If thread is joinable, allocate a termination condition variable */
if (joinable) {
trd->term = st_cond_new();
if (trd->term == NULL) {
_st_stack_free(trd->stack);
return NULL;
}
}
/* Make thread runnable */
trd->state = _ST_ST_RUNNABLE;
_st_active_count[THREAD_INDEX]++;
_ST_ADD_RUNQ(trd);
return trd;
}
3.3 协程的上下文切换
协程的上下文切换通过调度切换,当出现i/o或者协程退出后发生上下文切换,切换逻辑,遍历runnable列表
1.有可执行任务,直接恢复可执行任务的上下文
2. 没有有可执行任务,调用dispatch,知道出现了新的可执行的事件
切换源码如下:
// 切换阻塞协程
#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
// 恢复协程上下文
#define _ST_RESTORE_CONTEXT(_thread)
ST_BEGIN_MACRO
_ST_SET_CURRENT_THREAD(_thread);
MD_LONGJMP((_thread)->context, 1);
ST_END_MACRO
void _st_vp_schedule(void)
{
_st_thread_t *thread;
unsigned int cost_time;
static __thread char buf[80];
if (_ST_RUNQ.next != &_ST_RUNQ) {
/* Pull thread off of the run queue */
thread = _ST_THREAD_PTR(_ST_RUNQ.next);
_ST_DEL_RUNQ(thread);
} else {
/* 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的总结
st巧妙的利用了jump由用户程序自发的调用cpu,相比线程有私有栈的跳转切换小了很多,整个调度过程是FIFO,没有抢占模式。st源码vp等全局变量使得,st只支持单线程(多进程数据独立,可以分别使用)。由于团队业务的一些特殊性,需要st支持多线程,将st的全局变量进行响应的封装,可以将st改造成多线程版本。
五、st相关的知识
1. 主页源码:https://sourceforge.net/projects/state-threads/files/
2. 进程、线程、协程参考:http://kexianda.info/2017/05/19/进程-线程-协程的实现原理/
优质内容筛选与推荐>>
1、jquery中attr()与prop()方法的区别2、Confluence 6 升级自定义的站点和空间仔细测试你的修改3、NOIP2014-普及组复赛-第三题-螺旋矩阵4、实验35、Java EE 运用DBHelper连接数据库做一个简单的登录测试