纤程的操作
首先要注意的一个问题是,实现线程的是Wi n d o w s内核。操作系统清楚地知道线程的情况,并且根据M i c r o s o f t定义的算法对线程进行调度。纤程是以用户方式代码来实现的,内核并不知道纤程,并且它们是根据用户定义的算法来调度的。由于你定义了纤程的调度算法,因此,就内核而言,纤程采用非抢占式调度方式。
需要了解的下一个问题是,单线程可以包含一个或多个纤程。就内核而言,线程是抢占调度的,是正在执行的代码。然而,线程每次执行一个纤程的代码—你决定究竟执行哪个纤程(随着我们讲解的深入,这些概念将会越来越清楚)。
当使用纤程时,你必须执行的第一步操作是将现有的线程转换成一个纤程。可以通过调用C o n v e r t T h r e a d To F i b e r函数来执行这项操作:
PVOID ConvertThreadToFiber(PVOID pvParam);
该函数为纤程的执行环境分配相应的内存(约为2 0 0字节)。该执行环境由下列元素组成:
• 一个用户定义的值,它被初始化为传递给C o n v e r t T h r e a d To F i b e r的p v P a r a m参数的值。
• 结构化异常处理链的头。
• 纤程内存栈的最高和最低地址(当将线程转换成纤程时,这也是线程的内存栈)。
• CPU寄存器,包括堆栈指针、指令指针和其他。
当对纤程的执行环境进行分配和初始化后,就可以将执行环境的地址与线程关联起来。该线程被转换成一个纤程,而纤程则在该线程上运行。C o n v e r t T h r e a d To F i b e r函数实际上返回纤程的执行环境的内存地址。虽然必须在晚些时候使用该地址,但是决不应该自己对该执行环境数据进行读写操作,因为必要时纤程函数会为你对该结构的内容进行操作。现在,如果你的纤程(线程)返回或调用E x i t T h r e a d函数,那么纤程和线程都会终止运行。
除非打算创建更多的纤程以便在同一个线程上运行,否则没有理由将线程转换成纤程。若要创建另一个纤程,该线程(当前正在运行纤程的线程)可以调用C r e a t e F i b e r函数:
PVOID CreateFiber(
DWORD dwStackSize,
PFIBER_START_ROUTINE pfnStartAddress,
PVOID pvParam);
C r e a t e F i b e r首先设法创建一个新内存栈,它的大小由d w S t a c k S i z e参数来指明。通常传递的参数是0,按照默认设置,它创建一个内存栈,其大小可以扩展为1 M B,不过开始时有两个存储器页面用于该内存栈。如果设定一个非0值,那么就用设定的大小来保存和使用内存栈。
接着,C r e a t e F i b e r函数分配一个新的纤程执行环境结构,并对它进行初始化。该用户定义的值被设置为传递给C r e a t e F i b e r的p v P a r a m参数的值,新内存栈的最高和最低地址被保存,同时,纤程函数的内存地址(作为p f n S t a r t A d d r e s s参数来传递)也被保存。
P f n S t a r t A d d r e s s参数用于设定必须实现的纤程例程的地址,它必须采用下面的原型:
VOID WINAPI FiberFunc(PVOID pvParam);
当纤程被初次调度时,该函数就开始运行,并且将原先传递给C r e a t e F i b e r的p v P a r a m的值传递给它。可以在这个纤程函数中执行想执行的任何操作。但是该函数的原型规定返回值是V O I D,这并不是因为返回值没有任何意义,而是因为该函数根本不应该返回。如果纤程确实返回了,那么线程和该线程创建的所有纤程将立即被撤消。
与C o n v e r t T h r e a d To F i b e r函数一样,C r e a t e F i b e r函数也返回纤程运行环境的内存地址。但是,与C o n v e r t T h r e a d To F i b e r不同的是,这个新纤程并不执行,因为当前运行的纤程仍然在执行。在单个线程上,每次只能运行一个纤程。若要使新纤程能够运行,可以调用Switch To Fiber函数:
VOID SwitchToFiber(PVOID pvFiberExecutionContext);
Switch To Fiber 函数只有一个参数,即p v F i b e r E x e c u t i o n C o n t e x t,它是上次调用C o n v e r t T h r e a d To F i b e r或C r e a t e F i b e r函数时返回的纤程的执行环境的内存地址。该内存地址告诉该函数要对哪个纤程进行调度。S w i t c h To F i b e r函数在内部执行下列操作步骤:
1) 它负责将某些当前的C P U寄存器保存在当前运行的纤程执行环境中,包括指令指针寄存器和堆栈指针寄存器。
2) 它将上一次保存在即将运行的纤程的执行环境中的寄存器装入C P U寄存器。这些寄存器包括堆栈指针寄存器。这样,当线程继续执行时,就可以使用该纤程的内存栈。
3) 它将纤程的执行环境与线程关联起来,线程运行特定的纤程。
4) 它将线程的指令指针设置为已保存的指令指针。线程(纤程)从该纤程上次执行的地方开始继续执行。
S w i t c h To F i b e r函数是纤程获得C P U时间的唯一途径。由于你的代码必须在相应的时间显式调用S w i t c h To F i b e r函数,因此你对纤程的调度可以实施全面的控制。记住,纤程的调度与线程调度毫不相干。纤程运行所依赖的线程始终都可以由操作系统终止其运行。当线程被调度时,当前选定的纤程开始运行,而其他纤程则不能运行,除非显式调用S w i t c h To F i b e r函数。若要撤消纤程,可以调用D e l e t e F i b e r函数:
VOID DeleteFiber(PVOID pvFiberExecutionContext);
该函数用于删除p v F i b e r E x e c u t i o n C o n t e x t参数指明的纤程,当然这是纤程的执行环境的地址。该函数能够释放纤程栈使用的内存,然后撤消纤程的执行环境。但是,如果传递了当前与线程相关联的纤程地址,那么该函数就在内部调用E x i t T h r e a d函数,该线程及其创建的所有纤程全部被撤消。
D e l e t e F i b e r函数通常由一个纤程调用,以便删除另一个纤程。已经删除的纤程的内存栈将被撤消,纤程的执行环境被释放。注意,纤程与线程之间的差别在于,线程通常通过调用E x i t T h r e a d函数将自己撤消。实际上,用一个线程调用Te r m i n a t e T h r e a d函数来终止另一个线程的运行,是一种不好的方法。如果你确实调用了Te r m i n a t e T h r e a d函数,系统并不撤消已经终止运行的线程的内存栈。可以利用纤程的这种能力来删除另一个纤程,后面介绍示例应用程序时将说明这是如何实现的。
为了使操作更加方便,还可以使用另外两个纤程函数。一个线程每次可以执行一个纤程,操作系统始终都知道当前哪个纤程与该线程相关联。如果想要获得当前运行的纤程的执行环境的地址,可以调用G e t C u r r e n t F i b e r函数:
PVOID GetCurrentFiber();
另一个使用非常方便的函数是G e t F i b e r D a t a:
PVOID GetFiberData();
前面讲过,每个纤程的执行环境包含一个用户定义的值。这个值使用作为C o n v e r t T h r e a dTo F i b e r或C r e a t e F i b e r的p v P a r a m参数而传递的值进行初始化。该值也可以作为纤程函数的参数来传递。G e t F i b e r D a t a只是查看当前执行的纤程的执行环境,并返回保存的值。
无论G e t C u r r e n t F i b e r还是G e t F i b e r D a t a,运行速度都很快,并且通常是作为内蕴函数(infrinsic funcfion)来实现的,这意味着编译器能够为这些函数生成内联代码。
Windows内核实现线程,fibers实现时在用户模式。线程由操作系统根据算法调度,fibers由编程人员编写代码调度。
Fibers看起来就是一个轻量级线程的实现,但有的人说这种说法不对。到底怎么才算是轻量级线程呢?只看到Erlang有轻量级线程的说法,其它地方都没有,这个概念暂时放一边好了。
一个线程可以有一个或多个fibers,Fibers都是在一个线程中执行,在操作系统看来,它只能看到线程,只对线程做调度,线程是调度的基本单位。因此,同一个时间只会有一个fiber在执行。如果线程中的一个fiber进入阻塞状态,整个线程进入阻塞状态。参考UNIX线程库,两者在这方面差不多。
编写fibers程序的典型模式/步骤:
1. 线程把自己由线程转换为一个fiber,创建出第一个fiber。
PVOID ConvertThreadToFiber(PVOID pvParam);
2. 再由这个线程(现在是fiber)创建其它的fiber。
PVOID CreateFiber(
DWORD dwStackSize,
PFIBER_START_ROUTINE pfnStartAddress,
PVOID pvParam);
第一个fiber会继续执行转换函数后面的代码,以后创建的fiber需要传递一个函数地址给CreateFiber函数,作为执行入口。函数返回一个fiber上下文句柄,提供给控制函数使用。
3. 切换到新的fiber执行。
VOID SwitchToFiber(PVOID pvFiberExecutionContext);
这个函数可以在任何fiber中调用,调用后,调用者停止执行,pvFiberExcutionContext代表的fiber开始运行。
4. 切换回第一个fiber,准备结束线程。
在各个fiber调用后,某个fiber执行SwitchToFiber函数切换回第一个fiber中。因为第一个fiber是线程,要结束运行还是得从这里结束会比较好,当然也可以从其它fiber结束。任何一个fiber运行结束会使线程结束。但是那样不能很好的释放fiber的资源(stack 和 context)。
5. 删除已经创建的fiber(不包括第一个fiber,第一个fiber是线程转换得到)。
VOID DeleteFiber(PVOID pvFiberExecutionContext);
6. 把第一个fiber转换回线程,线程继续执行到结束。
#define _WIN32_WINNT 0x400
#include
#include
#define FIBER_COUNT 10
void *fiber_context[FIBER_COUNT];
VOID WINAPI fiberProc(void*);
int main()
{
int i;
int fibers[FIBER_COUNT];
for (i = 0; i < FIBER_COUNT; i++)
fibers[i] = i;
fiber_context[0] = ConvertThreadToFiber(NULL);
for (i = 1; i < FIBER_COUNT; ++i) {
fiber_context[i] = CreateFiber(0, fiberProc, &fibers[i]);
if (fiber_context[i] != NULL)
printf("Fiber %d created\n", i);
}
for (i = 1; i < FIBER_COUNT; ++i) {
if (fiber_context[i] != NULL)
SwitchToFiber(fiber_context[i]);
}
printf("Over\n");
}
void WINAPI fiberProc(void *fiber_nbr)
{
int nbr;
nbr = *((int*)fiber_nbr);
printf("Hello from fiber %d\n", nbr);
SwitchToFiber(fiber_context[0]);
}
#include
#include
#include
#include
#include "coroutine.h"
/* windows fiber版本的协程yield的时候直接切换到主协程(main),
而不是swapcontext的切换到上次运行协程,但最后达到的结果却一样
*/
// 默认容量
#define DEFAULT_CAP 8
// 堆栈大小
#define INIT_STACK 1048576 //(1024*1024)
typedef struct schedule schedule;
typedef struct coroutine coroutine;
typedef struct coroutine_para coroutine_para;
struct schedule
{
int cap; // 容量
int conums;
int curID; // 当前协程ID
LPVOID main;
coroutine **co;
};
struct coroutine
{
schedule *s;
void *ud;
int status;
LPVOID ctx;
coroutine_func func;
};
static int co_putin(schedule *s, coroutine *co)
{
if (s->conums >= s->cap)
{
int id = s->cap;
s->co = realloc(s->co, sizeof(coroutine *) * s->cap * 2);
memset(&s->co[s->cap], 0, sizeof(coroutine *) * s->cap);
s->co[s->cap] = co;
s->cap *= 2;
++s->conums;
return id;
}
else
{
for (int i = 0; i < s->cap; i++)
{
int id = (i + s->conums) % s->cap;
if (s->co[id] == NULL)
{
s->co[id] = co;
++s->conums;
return id;
}
}
}
assert(0);
return -1;
}
static void co_delete(coroutine *co)
{
//If the currently running fiber calls DeleteFiber, its thread calls ExitThread and terminates.
//However, if a currently running fiber is deleted by another fiber, the thread running the
//deleted fiber is likely to terminate abnormally because the fiber stack has been freed.
DeleteFiber(co->ctx);
free(co);
}
schedule *coroutine_open()
{
schedule *s = malloc(sizeof(schedule));
s->cap = DEFAULT_CAP;
s->conums = 0;
s->curID = -1;
s->co = malloc(sizeof(coroutine *) * s->cap);
memset(s->co, 0, sizeof(coroutine *) * s->cap);
s->main = ConvertThreadToFiberEx(NULL, FIBER_FLAG_FLOAT_SWITCH);
return s;
}
void coroutine_close(schedule *s)
{
for (int i = 0; i < s->cap; i++)
{
coroutine *co = s->co[i];
if (co) co_delete(co);
}
free(s->co);
s->co = NULL;
free(s);
}
void __stdcall coroutine_main(LPVOID lpParameter)
{
schedule* s = (schedule*)lpParameter;
int id = s->curID;
coroutine *co = s->co[id];
(co->func)(s, co->ud);
s->curID = -1;
--s->conums;
s->co[id] = NULL;
//co_delete(co);
SwitchToFiber(s->main);
}
int coroutine_new(schedule *s, coroutine_func *func, void *ud)
{
coroutine *co = malloc(sizeof(coroutine));
co->s = s;
co->status = COROUTINE_READY;
co->func = func;
co->ud = ud;
int id = co_putin(s, co);
co->ctx = CreateFiberEx(INIT_STACK, 0, FIBER_FLAG_FLOAT_SWITCH, coroutine_main, s);
co->status = COROUTINE_READY;
return id;
}
void coroutine_resume(schedule *s, int id)
{
assert(id >= 0 && id < s->cap);
if (id < 0 || id >= s->cap) return;
coroutine *co = s->co[id];
if (co == NULL) return;
switch (co->status)
{
case COROUTINE_READY:case COROUTINE_SUSPEND:
co->status = COROUTINE_RUNNING;
s->curID = id;
SwitchToFiber(co->ctx);
if (!s->co[id]) co_delete(co);
break;
default:
assert(0);
break;
}
}
void coroutine_yield(schedule *s)
{
int id = s->curID;
assert(id >= 0 && id < s->cap);
if (id < 0) return;
coroutine *co = s->co[id];
co->status = COROUTINE_SUSPEND;
s->curID = -1;
SwitchToFiber(s->main);
}
int coroutine_status(schedule *s, int id)
{
assert(id >= 0 && id < s->cap);
if (id < 0) return;
if (s->co[id] == NULL) {
return COROUTINE_DEAD;
}
return s->co[id]->status;
}
int coroutine_running(schedule *s)
{
return s->curID;
}
#include "coroutine.h"
void test3(schedule *s, void *ud)
{
int *data = (int*)ud;
for (int i = 0; i < 3; i++)
{
printf("test3 i=%d\n",i);
coroutine_yield(s);
printf("yield co id = %d.\n", *data);
}
}
void coroutine_test()
{
printf("coroutine_test3 begin\n");
schedule *s = coroutine_open();
int a = 11;
int id1 = coroutine_new(s, test3, &a);
int id2 = coroutine_new(s, test3, &a);
while (coroutine_status(s, id1) && coroutine_status(s, id2))
{
printf("\nresume co id = %d.\n",id1);
coroutine_resume(s, id1);
//printf("resume co id = %d.\n", id2);
//coroutine_resume(s, id2);
}
int id3 = coroutine_new(s, test3, &a);
while (coroutine_status(s, id3))
{
printf("\nresume co id = %d.\n", id3);
coroutine_resume(s, id3);
}
printf("coroutine_test3 end\n");
coroutine_close(s);
}
int main()
{
coroutine_test();
return 0;
}