Protothreads的原生特性
背景介绍
Protothreads 是一个无堆栈的系统,原创作者是Adam Dunkels adam@sics.se。根据原作者的介绍,Protothreads的原生特性主要有:
- 没有专用的机器代码,纯C实现;
- 不使用容易犯错的跳转指令; 任务调度的switch case 用宏封装
- 极小的内存占用;最小线程开销2字节的
- 当不当做操作系统来用都行;可以和混用轮询的裸机程序
- 所提供的阻断等待不需要堆栈或者full multi-threading。
原作者在github的开源仓库 https://github.com/gburd/pt.git
原生文件介绍
支持Protothreads系统的一共有5个头文件,分别是lc-addrlabels.h、lc-switch.h、lc.h、pt-sem.h、pt.h。其中lc-addrlabels.h和lc-switch.h功能一致,实现的方式略有差异。pt-sem.h文件时使用其无堆栈系统的特性实现的信号量。如果想使用Protothreads的原生系统,只需要移植文件lc-switch.h、lc.h、pt.h即可。
原生接口介绍
原生Protothreads暴露的接口主要时在pt.h文件里。主要有以下这些接口
接口函数 | 功能描述 |
---|---|
PT_BEGIN(pt) | 线程开始时调用: switch { |
PT_YIELD(pt) | 调用后让出CPU一次 : case |
PT_WAIT_UNTIL(pt, condition) | 调用后让出CPU直到条件成立:case |
PT_SPAWN(pt, child, thread) | 调用后CPU让给子线程:case |
PT_RESTART(pt) | 重置线程运行记录标记 |
PT_EXIT(pt) | 调用异常结束线程 |
PT_INIT(pt) | 初始化线程运行记录标记 |
PT_END(pt) | 调用后正常结束线程:} |
一个protothreads的必须得由,PT_BEGIN开始,PT_END结束。如果中间代码块使用while(1)的死循环函数,必须还要有PT_YIELD或PT_SPAWN或PT_WAIT_UNTIL等具备case标记的阻塞函数。以下是一个最小的protothreads的结构
static PT_THREAD(demo_thread(struct pt *pt))
{
static uint8_t s_cond = PT_FALSE;
PT_BEGIN(pt);
while (1)
{
PT_YIELD(pt);
PT_WAIT_UNTIL(pt, s_cond);
}
PT_END(pt);
}
上述线程里,会一直阻塞在PT_WAIT_UNTIL(pt,s_cond);因为s_cond 是局部static变量,条件永远无法变真。
调度原理分析
既然接口都是用宏来实现的,根据编译原理,我们可以使用gcc -E的方式把宏展开,通过展开后的宏,我们可以一浏览其结构。从而分析Protothreads的调度原理,从根本上理解无堆栈系统的含义。由于Proththreads使用了__LINE__宏,为了贴切表述具体含义,下述两个分析例子将使用贴图的方式。
例子一:最小的线程结构
原始的min_thread
gcc -E 展开后的min_thread
如果在未展开的代码中去掉LOG的标记, 其简洁版如下
static PT_THREAD(min_thread(struct pt *pt))
{
PT_BEGIN(pt);
PT_YIELD(pt);
PT_END(pt);
}
对比原始代码和gcc -E后的代码.我们先在应用的层面理解
- 我们对展开后的代码进行脑颅测试,如果轮询调用函数,第一次会从正常运行到2080行return,第二次则会在2071行直接跳转到2079行,并且顺利走到2091行。
- 对照简洁版的代码,我们可以看出min_threads的功能是第一次运行PT_BEGIN然后再PT_YIELD跳出,第二次会直接从PT_YIELD下面运行到PT_END
对比原始代码和gcc -E后的代码.我们先在原理的层面理解
- 原始的10行是展开后的2069到2072,也就是说PT_BEGIN 提供一个不完整的switch语句头部
- 原始的11到13行对应着2073到2075 ,配合着PT_YEILD展开看来,原始的11到13行仅会运行一次
- 同理原始的15到17行对应着2084到2086,配合着PT_END展开看来,原始的15到17行仅会运行一次,且PT_END后面的绝对不会运行到
由此可见最小的prothreads ,PT_BEGIN和PT_END 成对存在,组成一个完成的switch case 语句。而PT_YIELD提供一个让出CPU的功能。prothreads提供了一个switch case的任务调度的雏形。
例子二:线程带while(1)结构
原始的min_thread
gcc -E 展开后的min_thread
如果在未展开的代码中去掉LOG的标记, 其简洁版如下
static PT_THREAD(while_thread(struct pt *pt))
{
static uint8_t s_cond = PT_FALSE;
PT_BEGIN(pt);
while (1)
{
PT_YIELD_UNTIL(pt, s_cond);
}
PT_END(pt);
}
对比原始代码和gcc -E后的代码.我们先在应用的层面理解
- 我们对展开后的代码进行脑颅测试,如果轮询调用函数,第一次会从正常运行到2013行return,第二次则会在2093行直接跳转到2101行,并且2013行return。(除非s_cond成真)
- 对照简洁版的代码,我们可以看出min_threads的功能是第一次运行PT_BEGIN然后再PT_YIELD_UNTIL跳出,第二次会直接从PT_YIELD_UNTIL判断s_cond的条件。除非s_cond成真才会出来。
- 假设s_cond成真了,程序通过了2106,在2107的时候,会再次出现在2096的while(1)里面。结果再次进行s_cond的判断
由此可见最小的prothreads ,PT_BEGIN和PT_END 成对存在,该对里面还是遵循C语言的规则的。(注意:switch语句有条件遵循)
优缺点分析
Prothreads基础调度原理类似状态机的方式,但通过宏的封装,让使用者可以从状态机的切换中解脱出来。按照类操作系统的方式进行程序设计,无需费劲心思设计异步的方案。同样的通过调度的原理(宏的展开),我们非常贴切的明白了Prothreads的无栈 含义。**线程里局部变量将会被释放!!!**以下整理出来的优点和缺点
优点 | 缺点 |
---|---|
极少的线程开销 | 无堆栈 |
纯C实现无专用机器码 完美兼容轮询式裸机 | switch语法受限 |
switch 受限的例子见附录,具体原因自行分析
结论
根据Prothreads的自身原理分析,其缺点我们可以通过代码编写规范进行规避。例如:
- Prothreads系统所有线程必须主动让出CPU的使用权。(非抢占式操作系)
- Prothreads的PT_BEGIN 到PT_END使用局部变量需要谨慎,局部变量赋值后,不能在引发任务调用的宏之后调用。
- Prothreads的线程里面如果使用switch ,case里面不能使用任务调用宏。
但是如果我们使用RTOS和Prothreads进行对比,暂时抛开代码生态说,即使Prothreads具备卓越的低资源,当使用的舒适度完全比不过RTOS。例如Prothreads不具备最基础的OS的sleep的功能,不具备消息邮箱,信号量(超时机制),消息队列,低功耗支持,线程状态监控,事件机制等等,一些通用的RTOS同步接口。
为了解决以上问题,我在原生的Prothreads上进行进一步的拓展,让其同样居于RTOS的丰富的同步接口,并且同时具备极低的系统资源开销。对于一些本来资源较低的芯片,在无法过多使用RTOS丰富的轮子,这将是一个更好的的操作系统。
详情可见《第一章: Protothreads的拓展》后续为了方便描述,统一命名为PT_EX
以下是我在github上进行开源的仓库地址:https://github.com/liufuzhao/Protothreads.git
附录
错误的switch使用方法
gcc -E 宏展开后