Protothreads库学习(一) - 阻塞等待

Protothreads库介绍

Protothreads是一个极其轻量的协程库,资源开销极小,适合于具有协程需求但单片机资源不足以运行RTOS的嵌入式系统等环境。

Protothreads库安装

Protothreads - Lightweight, Stackless Threads in C下载源代码即可,包括三个.c示例文件与五个.h头文件,其中头文件是库本体。lc-switch.hlc-addrlabels.h是该库的两种实现方案,需根据编译器与需求进行二选一,默认使用lc-switch.h,下面的讲解也是基于lc-switch.h进行讲解。

阻塞等待应用

打开example-small.c文件,其中包含两个线程函数与一个main函数。

main函数前,先定义了两个静态结构体变量,这是用于保存线程断点的结构体,其中仅包含一个正整数变量lc

1
2
3
4
5
6
7
typedef unsigned short lc_t;

struct pt {
lc_t lc;
};

static struct pt pt1, pt2;

main函数中,先使用PT_INIT宏对pt结构体变量进行初始化,这个宏仅仅是把结构体中的lc变量置为零。

1
2
3
4
5
6
7
#define LC_INIT(s) s = 0;

#define PT_INIT(pt) LC_INIT((pt)->lc)

/* Initialize the protothread state variables with PT_INIT(). */
PT_INIT(&pt1);
PT_INIT(&pt2);

然后在一个无限循环中轮询调用两个线程函数。

1
2
3
4
5
6
7
8
9
/*
* 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);
}

我们在看第一个线程函数的实现,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 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);
}

这是一个静态函数,接收pt结构体指针变量,返回整形值。返回值我们无需在意,宏会帮我们处理好。

我们发现函数被两个宏包围着,分别是PT_BEGINPT_END,同时应用部分的代码被一块while(1)循环包裹。

循环中调用了PT_WAIT_UNTIL宏,该宏会阻塞线程直至条件成立,并让出CPU给其他线程。此时我们看第二个线程,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 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);
}

其结构与第一个线程大同小异,在应用层的代码中,先把protothread2_flag置1,然后阻塞让出CPU资源。这时代码又回到线程一,阻塞条件不再成立,代码继续执行,在控制台上打印,将protothread2_flag置0,protothread1_flag置1,然后新的循环开始,线程重新阻塞,CPU回到线程二,阻塞结束,打印字符,protothread1_flag置0,开始新的循环。于是最终运行结果就是控制台上两句话交替输出。

PT_WAIT_UNTIL类似的还有PT_WAIT_WHILE宏,其功能与PT_WAIT_UNTIL相反。PT_WAIT_UNTIL是直到条件成立才结束阻塞,而PT_WAIT_WHILE是条件成立时一直阻塞,条件不满足时才结束阻塞。

还有一点值得注意的是,Protothreads并不会保存一个线程阻塞时的运行状态,也就是说,在线程阻塞前定义的局部变量将全部是不可信的,这是该库所采用的实现方案所不可避免的。解决方案唯有使用全局变量或静态变量才可行。

除此之外,也不能在PT_BEGINPT_END之间使用switch语句,因为该库实现利用了switch,嵌套switch会导致问题。

阻塞等待原理

要清楚Protothreads的实现原理,需要了解一个特殊的C语言特性,详情见下面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main(int argc, char const *argv[]) {
int n = 1;
int i = 0;
switch (n) {
case 0:
int j = 2;
for (i = 0; i < 10; i++) {
case 1:
printf("%d, %d\n", i, j);
}
}

return 0;
}

在其他编程语言中,这种情况是不合语法的,但在C语言中,这种情况是合理的。

我们很容易知道n为0时,代码会正常输出ij的值,但当n为1时,代码会如何运行呢?

答案是代码会直接跳转到case 1:后面运行,此时j就成为了未定义的变量,输出值不可预期(UB)。

上面的代码就是Protothreads的原理,我们调用的宏PT_BEGINPT_END就是创建了一个与上面代码类似的switch-case代码块,当调用阻塞时就是创建了一个类似上面case 1:的语句,在pt中写入相应的变量,此后switch就从此处开始执行,这也是应用部分提及到的局部变量失效的原因。


Protothreads库学习(一) - 阻塞等待
https://blog.faneter.top/2026/08/20/Protothreads库学习-一-阻塞等待/
作者
Faneter
发布于
2026年8月20日
许可协议