进程,一个具有独立功能的正在运行的程序实例。进程是相互独立的,并且可以同时运行。
线程,描述一段代码的执行路径。线程属于进程,每个进程至少有一个线程。线程有自己独立的栈,多个线程可以同时运行。
协程,coroutine,可以看作co-routine,也即协作程序。几个程序协作运行,可以理解为轻量级线程。
进程和线程都是重量级的,功能更强大,但是开销同样更大。尤其是针对一些嵌入式设备,受限于空间和性能,无法使用进程和线程。此时有一些并发需要,如果沿用通常的顺序流程来开发,代码的逻辑处理会比较复杂。面对这种情况,需要有一个类似并发的代码开发模型,协程就是一个简单的并发开发模型。
ProtoThread是一个极简的C语言协程库,由5个简单的.h文件构成。ProtoThread主要是利用switch case内嵌循环的特殊语法来实现的。因为ProtoThread完全是利用C语言的语法特性,所以ProtoThread可以适用所有C/C++项目。
达夫设备(Duff’s device)是串行复制(serial copy)的一种优化实现,实现展开循环,进而提高执行效率。达夫设备的提出,为后面协程代码提供了基础条件。
void send( int * to, int * from, int count)
{
int n = (count + 7 ) / 8 ;
switch (count % 8 ) {
case 0 : do { * to ++ = * from ++ ;
case 7 : * to ++ = * from ++ ;
case 6 : * to ++ = * from ++ ;
case 5 : * to ++ = * from ++ ;
case 4 : * to ++ = * from ++ ;
case 3 : * to ++ = * from ++ ;
case 2 : * to ++ = * from ++ ;
case 1 : * to ++ = * from ++ ;
} while (-- n > 0);
}
}
初看上述代码,感觉非常奇怪,这样的代码竟然能够执行。switch case在编译后,生成跳转表。
下面的代码是Python中协程的一个应用,发生器。每次调用都会自动从上一次的代码处执行。
def foo(num):
while num < 10:
num = num+1
yield num
我们可以用goto来模拟类似的效果。看下面代码:
int function(void)
{
static int i = 0;
static int state = 0;
if (0 == state)
{
goto LABEL0;
}
else
{
goto LABEL1;
}
LABEL0:
for (i = 0; i < 10; i++)
{
state = 1;
return i;
LABEL1:;
}
return 0;
}
int main()
{
for (int i = 0; i < 10; i++)
{
function();
}
return 0;
}
function()在第一次调用时,跳转到LABLE0:;第二次调用时直接跳转到LABLE1:,再往下执行则到了for循环自加并进行判断比较。goto和switch case都是跳转表,可以互换。
int function1(void)
{
static int i = 0;
static int state = 0;
switch (state)
{
case 0:
for (i = 0; i < 10; i++)
{
state = 1;
return i;
case 1:;
}
}
return 0;
}
我们将function1按Python中的协程形式进行修改:
下面使用了编译宏__LINE__,可以让case更具唯一性,减少命名的负担。do while(0)只是用来保证宏的安全性。每次调用function3时,即从YIELD(i)处退出,下次调用即进入for自加并判断。
为什么要用宏来将代码进行一个修改?上述代码虽然语法正确,但是语法形式上非常的怪异,尤其是当逻辑代码更多时,非常难以理解。为了突出算法的逻辑,而优化语法的形式是值得的。
#define BEGIN() static int state=0; switch(state) { case 0:
#define YIELD(x) do { state=__LINE__; return x; case __LINE__:; } while (0)
#define END() }
int function3(void)
{
static int i = 0;
BEGIN();
for (i = 0; i < 10; i++)
{
YIELD(i);
}
END();
return 0;
}
void main()
{
// 下面两个function3函数,执行一次for循环即交由另外一个函数执行一次for循环
// 表现上和真正的协程基本一样
for (;;)
{
function3();
function3();
}
}
上述的协程,基本功能比较简单。ProtoThread在此基础上,添加了一些等待跳转的功能。
struct pt {
lc_t lc;
};
typedef unsigned short lc_t;
#define LC_INIT(s) s = 0;
#define LC_RESUME(s) switch(s) { case 0:
#define LC_SET(s) s = __LINE__; case __LINE__:
#define LC_END(s) }
#define PT_BEGIN(pt) { char PT_YIELD_FLAG = 1; LC_RESUME((pt)->lc)
#define PT_END(pt) LC_END((pt)->lc); PT_YIELD_FLAG = 0; \
PT_INIT(pt); return PT_ENDED; }
下面的示例,两个function()函数每执行一次即跳到对方函数执行,两个函数的功能看起来是并行的。
#define PT_YIELD(pt) \
do { \
PT_YIELD_FLAG = 0; \
LC_SET((pt)->lc); \
if(PT_YIELD_FLAG == 0) { \
return PT_YIELDED; \
} \
} while(0)
void Function(void)
{
static int i = 0;
static struct pt ptFlag;
// 代码每次执行到此处即跳转到PT_YIELD之后的代码
PT_BEGIN(&ptFlag);
for (i = 0; i < 10; i++)
{
// 每次执行时记录当前位置行号,并退出当前函数
PT_YIELD(&ptFlag);
printf("Function:%d\n", i);
}
PT_END(&ptFlag);
}
void main()
{
int nIdx = 0;
for (nIdx = 0; nIdx < 10; nIdx++)
{
Function();
Function();
}
}
PT_YIELD最简单,直接让出当前代码的执行权限。PT_WAIT_UNTIL则需要根据条件语句来进行判断,直到条件不满足时,才会出让当前执行权限,功能上相较于PT_YIELD更灵活。
#define PT_WAIT_UNTIL(pt, condition) \
do { \
LC_SET((pt)->lc); \
if(!(condition)) { \
return PT_WAITING; \
} \
} while(0)
/**
* This is a very small example that shows how to use
* protothreads. The program consists of two protothreads that wait
* for each other to toggle a variable.
*/
/* We must always include pt.h in our protothreads code. */
#include "pt.h"
#include /* For printf(). */
/* Two flags that the two protothread functions use. */
static int protothread1_flag = 0;
static int protothread2_flag = 0;
/**
* The first protothread function. A protothread function must always
* return an integer, but must never explicitly return - returning is
* performed inside the protothread statements.
*
* The protothread function is driven by the main loop further down in
* the code.
*/
static int protothread1(struct pt *pt)
{
/* A protothread function must begin with PT_BEGIN() which takes a
pointer to a struct pt. */
PT_BEGIN(pt);
/* We loop forever here. */
while (1)
{
/* Wait until the other protothread has set its flag. */
PT_WAIT_UNTIL(pt, protothread2_flag != 0);
printf("Protothread 1 running\n");
/* We then reset the other protothread's flag, and set our own
flag so that the other protothread can run. */
protothread2_flag = 0;
protothread1_flag = 1;
/* And we loop. */
}
/* All protothread functions must end with PT_END() which takes a
pointer to a struct pt. */
PT_END(pt);
}
/**
* The second protothread function. This is almost the same as the
* first one.
*/
static int protothread2(struct pt *pt)
{
PT_BEGIN(pt);
while (1)
{
/* Let the other protothread run. */
protothread2_flag = 1;
/* Wait until the other protothread has set its flag. */
PT_WAIT_UNTIL(pt, protothread1_flag != 0);
printf("Protothread 2 running\n");
/* We then reset the other protothread's flag. */
protothread1_flag = 0;
/* And we loop. */
}
PT_END(pt);
}
/**
* Finally, we have the main loop. Here is where the protothreads are
* initialized and scheduled. First, however, we define the
* protothread state variables pt1 and pt2, which hold the state of
* the two protothreads.
*/
static struct pt pt1, pt2;
int main(void)
{
/* Initialize the protothread state variables with PT_INIT(). */
PT_INIT(&pt1);
PT_INIT(&pt2);
/*
* Then we schedule the two protothreads by repeatedly calling their
* protothread functions and passing a pointer to the protothread
* state variables as arguments.
*/
while (1)
{
protothread1(&pt1);
protothread2(&pt2);
}
}
PT_WAIT_UNTIL根据自定义条件判断是否出让执行权限,但是有时有这样的需求,生产者生产达到10件商品时,通知消费者去取。这种需求,需要有类似信号量的一种同步方法。ProtoThread提供了类似的机制,通过计数来判断是否出让执行权限。每等一次,计数减1,每设置一次信号,则记数加1。
示例是类型的生产者与消费者模型。
struct pt_sem {
unsigned int count;
};
#define PT_SEM_INIT(s, c) (s)->count = c
#define PT_SEM_WAIT(pt, s) \
do { \
PT_WAIT_UNTIL(pt, (s)->count > 0); \
--(s)->count; \
} while(0)
#define PT_SEM_SIGNAL(pt, s) ++(s)->count
#ifdef _WIN32
#include
#else
#include
#endif
#include
#include "pt-sem.h"
#define NUM_ITEMS 32
#define BUFSIZE 8
static int buffer[BUFSIZE];
static int bufptr;
static void add_to_buffer(int item)
{
printf("Item %d added to buffer at place %d\n", item, bufptr);
buffer[bufptr] = item;
bufptr = (bufptr + 1) % BUFSIZE;
}
static int get_from_buffer(void)
{
int item;
item = buffer[bufptr];
printf("Item %d retrieved from buffer at place %d\n",
item, bufptr);
bufptr = (bufptr + 1) % BUFSIZE;
return item;
}
static int produce_item(void)
{
static int item = 0;
printf("Item %d produced\n", item);
return item++;
}
static void consume_item(int item)
{
printf("Item %d consumed\n", item);
}
static struct pt_sem full, empty;
static PT_THREAD(producer(struct pt *pt))
{
static int produced;
PT_BEGIN(pt);
for (produced = 0; produced < NUM_ITEMS; ++produced)
{
// full描述等待次数,满了则retrun出去
PT_SEM_WAIT(pt, &full);
add_to_buffer(produce_item());
// 标记信息就相当于记数
PT_SEM_SIGNAL(pt, &empty);
}
PT_END(pt);
}
static PT_THREAD(consumer(struct pt *pt))
{
static int consumed;
PT_BEGIN(pt);
for (consumed = 0; consumed < NUM_ITEMS; ++consumed)
{
// Producer记的数如果清空了则return
PT_SEM_WAIT(pt, &empty);
consume_item(get_from_buffer());
PT_SEM_SIGNAL(pt, &full);
}
PT_END(pt);
}
static PT_THREAD(driver_thread(struct pt *pt))
{
static struct pt pt_producer, pt_consumer;
// PT_BEGIN第一次执行时会顺序执行,再次执行时,就会跳转到上次退出的位置
PT_BEGIN(pt);
PT_SEM_INIT(&empty, 0);
PT_SEM_INIT(&full, BUFSIZE);
PT_INIT(&pt_producer);
PT_INIT(&pt_consumer);
// pt只是用来构建switch case的结构
// 以后直接跳转到PT_WAIT_THREAD后面的判断条件,判断是继续循环还是退出
PT_WAIT_THREAD(pt, producer(&pt_producer) & consumer(&pt_consumer));
PT_END(pt);
}
int main(void)
{
struct pt driver_pt;
PT_INIT(&driver_pt);
while (PT_SCHEDULE(driver_thread(&driver_pt)))
{
/*
* When running this example on a multitasking system, we must
* give other processes a chance to run too and therefore we call
* usleep() resp. Sleep() here. On a dedicated embedded system,
* we usually do not need to do this.
*/
#ifdef _WIN32
Sleep(0);
#else
usleep(10);
#endif
}
return 0;
}
优点:
缺点: