近一年没用 ProtoThread 了,忘得差不多了。这次搞硬件密码器重新学习了下,同时记录下来笔记,方便下次使用。
ProtoThread 是一个资源占用极少的多线程协程库,可以在51、avr等资源极其有限的硬件上面运行。
ProtoThread 有几种实现方式,这里只考虑存c switch 实现。这个实现其实就是用1byte存放当前执行到的位置,下次恢复这个线程的时候通过 switch 跳转到上次执行的位置继续执行。每个线程就只占用2byte存放当前位置(当前代码行号)即可。伪代码:
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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | //线程结构体 struct pt { unsigned short lc; // 当前线程执行到的位置(行号) }; PT_THREAD(f1(struct pt *pt)) // 展开宏后的代码 // char f1(struct pt *pt) { // 这里可以写每次任务切换进来都执行的代码 // 注意,只有静态变量才能正确的保存,非静态变量在切换协程时会丢失 // 同样,静态变量的限制使得一个函数不能多协程同时执行,局部变量会冲突。 static int a; PT_BEGIN(pt); // 准备协程环境 // 展开宏后的代码: // char PT_YIELD_FLAG = 1; // switch(pt.lc) { case 0; // 等待至条件达成 // 可以看到,是通过记录当前执行位置后直接退出函数来实现的让出cpu。 // 恢复执行时通过 switch 的 case 跳转回上次执行的位置来实现的恢复执行。 PT_WAIT_UNTIL(pt, 条件); // 展开后的代码: /* do { (pt)->lc = __LINE__; case __LINE__:; if(!(condition)) { return PT_WAITING; } } while(0) */ PT_END(pt); // 结束协程环境 // 展开后的代码: // }; // PT_YIELD_FLAG = 0; // pt.lc=0; // return PT_ENDED; } void main() { struct pt p1; // 线程1 struct pt p2; // 线程2 //初始化线程 PT_INIT(&p1); // 其实就是 p1.lc = 0 PT_INIT(&p2); while(1) { /// 启动两个线程 f1(&p1); f1(&p2); } } |
这种实现的几个特点:
• 纯c实现,无硬件依赖,方便移植
• 不能在函数内再次使用switch
• 资源占用极少,每线程仅占用2字节
• 支持阻塞操作,没有栈的切换
• 运行在单一c函数中,无法跨函数是用。弥补措施是被调用函数也写成单独的Protothread
• 线程切换时不保存局部变量,弥补措施是使用静态局部变量,造成单个函数不能多线程同时执行。另一个弥补措施是实现自己的 pt 结构,将需要保存的局部变量保存到 pt 结构里面。
附几个函数:
PT_INIT(pt) 初始化任务变量,只在初始化函数中执行一次就行
PT_BEGIN(pt) 启动任务处理,放在函数开始处
PT_END(pt) 结束任务,放在函数的最后
PT_WAIT_UNTIL(pt, condition) 等待某个条件(条件可以为时钟或其它变量,IO等)成立,否则直接退出本函数,下一次进入本 函数就直接跳到这个地方判断
PT_WAIT_WHILE(pt, cond) 和上面一个一样,只是条件取反了
PT_WAIT_THREAD(pt, thread) 等待一个子任务执行完成
PT_SPAWN(pt, child, thread) 新建一个子任务,并等待其执行完退出
PT_RESTART(pt) 重新启动某个任务执行
PT_EXIT(pt) 任务后面的部分不执行,直接退出重新执行
PT_YIELD(pt) 锁死任务
PT_YIELD_UNTIL(pt, cond) 锁死任务并在等待条件成立,恢复执行
在pt中一共定义四种线程状态,在任务函数退出到上一级函数时返回其状态
PT_WAITING 等待
PT_EXITED 退出
PT_ENDED 结束
PT_YIELDED 锁死
(下面的是定时器,该宏是 http://www.cnblogs.com/xiaowuyi/p/4319720.html 写的,用之前请在#include “pt.h” 的前面,前面啊!加上一句#define PT_USE_TIMER)
先说明一下,下面的定时不一定完全准确的,可能会有点点的误差,可能偏后。如果遇上了很烦的任务,有可能会使延时延后。但是正常情况下,直接用就好了。
如果要很精确的延时,请用delay语句或者计时器,但是,绝大多数情况下,绝大多数情况!绝大多数情况!请用下面的语句代替delay延时!这样才能把CPU让给别的任务使用。
PT_TIMER_DELAY(pt,延时毫秒数);
字面上的意思,不用多说了吧?最大值约为49.7天,估计没人会延时辣么久吧……
PT_TIMER_MICRODELAY(pt,延时微秒数);
字面上的意思,不用多说了吧?注意,最小精度与arduino的版本有关,与micros()有精度一致。
PT_TIMER_WAIT_TIMEOUT(pt,条件,毫秒数);
如果条件成立了,或者超时了,就继续运行,否则切换任务。
要用的话,请在#include “pt.h”前面加上一句 #define PT_USE_SEM
首先要创建一个信号量,这个一定是全局变量:
static struct pt_sem 信号量名;
接着请在setup()函数里面给它初始化:
PT_SEM_INIT(&信号量名,数量);
信号量名前面有个&,别忘了。数量就相当于停车场的总车位数。
然后要用啦。任务要停一辆车进去:
PT_SEM_WAIT(pt,&信号量名);
信号量名前面有个&,别忘了。一个语句只能停一辆车,土豪好多车就用多次。
任务要开一辆车出来:
PT_SEM_SIGNAL(pt,&信号量名);
信号量名前面有个&,别忘了。用一次出一辆。
参考:
• http://dunkels.com/adam/pt/
• Protothread机制文档 http://blog.csdn.net/tietao/article/details/8507455
• 一个“蝇量级” C 语言协程库 http://coolshell.cn/articles/10975.html
• ProtoThreads http://blog.csdn.net/utopiaprince/article/details/6041385