准确的说是除掉头文件,测试代码和非关键的纯算法代码(只有双向环形链表的ADT),核心代码只有130行左右,已经是蝇量级的用户态线程库了。把这个库取名为ezthread,意思是,这太easy了,人人都可以读懂并且实现这个用户态线程库。我把该项目放在github上,欢迎来拍砖: https://github.com/Yuandong-Chen/coroutine/tree/old-version(注意,最新的版本已经用了共享栈技术,能够支持1000K数量级的协程了,读完这篇博文后可以进一步参考后续的博文:http://www.cnblogs.com/github-Yuandong-Chen/p/6973932.html)。那么下面谈谈怎么实现这个ezthread。
大家都会双向环形链表(就是头尾相连的双向链表),我们构造这个ADT结构:
首先是每个节点:
1 typedef struct __pnode pNode;
2 struct __pnode
3 {
4 pNode *next;
5 pNode *prev;
6 Thread_t *data;
7 };
显然,next指向下一个节点,prev指向上一个节点,data指向该节点数据,那么这个Thread_t是什么类型的数据结构呢?
typedef struct __ez_thread Thread_t;
struct __ez_thread
{
Regs regs;
int tid;
unsigned int stacktop;
unsigned int stacksize;
void *stack;
void *retval;
};
这个结构体包含了线程内部的信息,比如第一项为Regs,记录的是各个寄存器的取值(我们在下面给出具体的结构),tid就是线程的ID了,stacktop记录的是线程栈的顶部(和页对齐的最大地址,每个线程都有自己的运行时的栈,用于构成他们相对独立的运行时环境),stacksize就是栈的大小了,stack指针指向我们给该线程栈分配的堆的指针(什么?怎么一会栈一会堆的?我们其实用了malloc函数分配出一些堆空间,把这些空间用于线程栈,当线程退出时候,我们再free这些堆),retval就是线程运行完了的返回值(pthread_join里头拿到的线程返回值就是这个了)。
下面是寄存器结构体:
typedef struct __thread_table_regs Regs;
struct __thread_table_regs
{
int _ebp;
int _esp;
int _eip;
int _eflags;
};
真是好懂,一看就知道了,这个结构体只能支持X86体系的计算机了。那么还有个问题,为何只有这些寄存器,没用其他的比如:eax,ebx,edi,esi等等呢?因为我们在转换状态函数switch_to里头当返回时(准确地说是从上次切换的点切换回来时)用了return来切换回线程运行时环境,return会自动帮助我们把这些其他的寄存器的值恢复原状的(具体我们放到switch_to的时候再详细说明)。
然后呢,我们定义了一个游标去取这个环形链表的值,否则我们怎么读取这个环形链表里头的数据呢?总得有个东西指向其中某个节点吧。
typedef struct __loopcursor Cursor;
struct __loopcursor
{
int total;
pNode *current;
};
这个游标结构体记录了现在指向的节点地址和这个环形链表里头一共有多少节点。
我们得用两个这样的环形链表结构体来支持我们的线程库,为何是俩呢?一个是正在运行的线程,我们把他们串成一个环形链表,取名为live(活的),然后用另外一个链表把运行结束的线程串成一串,取名为dead(死的)。然后最开始我们就有个线程在运行了,那就是主线程main,我们用pmain节点来记录主线程:
extern Cursor live;
extern Cursor dead;
extern Thread_t pmain;
好了,剩下的只有在这些结构体上操作的函数了:
void init();
void switch_to(int, ...);
int threadCreat(Thread_t **, void *(*)(void *), void *);
int threadJoin(Thread_t *, void **);
我们开始时调用init,以初始化我们的live,dead和pmain。然后当我们想创造线程时,就threadCreat就可以了,用法和pthread_create基本一模一样,熟悉posix多线程的人一看就明白了,threadJoin也是仿照pthread_join接口写的。这里的switch_to就是最关键的运行时环境转换函数了,当线程调用这个函数时候,我们就切换到其他线程上次暂停的点去执行了(这些状态都保存在我们的Thread_t结构体里,所以我们能够记录下切换前的状态,从而能够从容地去切换到下一个线程中)。我们没有用定时器每隔几微秒去激发switch_to(实现起来也是非常简单的,但是得添加多个signal_block函数,非常不简洁),而是让线程里头的函数主动调用switch_to来切换线程,这有点类似协程。
好了,现在讲具体的实现了。首先是对双向链表的操作函数,这个东西不是我们的重点,懂基础算法数据结构的人都能实现,具体是双向环形链表的增查删操作:
1 void initCursor(Cursor *cur)
2 {
3 cur->total = 0;
4 cur->current = NULL;
5 }
6
7 Thread_t *findThread(Cursor *cur, int tid)
8 {
9 int counter = cur->total;
10 if(counter == 0){
11 return NULL;
12 }
13
14 int i;
15 pNode *tmp = cur->current;
16 for (int i = 0; i < counter; ++i)
17 {
18 if((tmp->data)->tid == tid){
19 return tmp->data;
20 }
21
22 tmp = tmp->next;
23 }
24 return NULL;
25 }
26
27 int appendThread(Cursor *cur, Thread_t *pth)
28 {
29 if(cur->total == 0)
30 {
31 cur->current = (pNode *)malloc(sizeof(pNode));
32 assert(cur->current);
33 (cur->current)->data = pth;
34 (cur->current)->prev = cur->current;
35 (cur->current)->next = cur->current;
36 cur->total++;
37 return 0;
38 }
39 else
40 {
41 if(cur->total > MAXCOROUTINES)
42 {
43 assert