纤程

纤程的操作

首先要注意的一个问题是,实现线程的是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;
}

你可能感兴趣的:(线程同步)