纯C无操作系统轻量协程库Protothread使用记录

目的

在单片机开发中很多时候都是无操作系统环境,这时候如果要实现异步操作,并且流程逻辑比较复杂时处理起来会稍稍麻烦。这时候可以试试 Protothread 这个协程库。

官网: https://dunkels.com/adam/pt/

Protothreads are extremely lightweight stackless threads designed for severely memory constrained systems, such as small embedded systems or wireless sensor network nodes. Protothreads provide linear code execution for event-driven systems implemented in C. Protothreads can be used with or without an underlying operating system to provide blocking event-handlers. Protothreads provide sequential flow of control without complex state machines or full multi-threading.


Protothreads是为内存严重受限的系统(如小型嵌入式系统或无线传感器网络节点)设计的极为轻量级的无堆栈线程。协议线程为在C中实现的事件驱动系统提供线性代码执行。协议线程可以与底层操作系统一起使用,也可以不与底层操作体系一起使用,以提供阻塞事件处理程序。原线程提供顺序控制流,而无需复杂的状态机或完整的多线程。

这篇文章主要是我自己使用入门记录。具体是实现原理细节等可以参考官网的文档或是下面文章:
《一个“蝇量级” C 语言协程库》https://coolshell.cn/articles/10975.html

源码说明

从官网下载Protothread库解压后里面就包含了源码、例程和文档:
在这里插入图片描述
在这里插入图片描述

整个库总共就五个头文件:

  • pt.h 协程库用户接口;
  • lc.h 用来选择具体协程的实现方式(默认为 lc-switch.h ,可以手动在这里更改);
  • lc-switch.h 使用 C语言 switch/case 语法实现的协程(使用该方式时协程函数中不能使用 switch/case ,可能会冲突);
  • lc-addrlabels.h 使用 gcc label 特性实现的协程(这个依赖GCC编译器);
  • pt-sem.h 信号量实现;

pt.h 中几个数据接口和接口如下:

// 协程控制数据结构
struct pt {
  lc_t lc;
};

// lc-switch.h中lc_t原型为typedef unsigned short lc_t;
// lc-addrlabels.h中lc_t原型为typedef void * lc_t;

// 以下是协程调度过程中的一些返回状态
#define PT_WAITING 0
#define PT_YIELDED 1
#define PT_EXITED  2
#define PT_ENDED   3

PT_INIT(pt) // 初始化控制数据结构(设置lc=0)

PT_THREAD(name_args) // 声明一个协程的函数(这个用不用无所谓,官方的例程有时候也没用)
PT_BEGIN(pt) // 协程入口
PT_END(pt) // 协程出口

PT_WAIT_UNTIL(pt, condition) // 等待condition为真向下运行,否则跳出当前协程
PT_WAIT_WHILE(pt, cond) // 和PT_WAIT_UNTIL相反,当cond为假向下运行,否则跳出当前协程

PT_WAIT_THREAD(pt, thread) // 等待子协程thread调度完成
PT_SPAWN(pt, child, thread) // 启动子协程thread,并等待其完成。child是子协程的pt

PT_RESTART(pt) // 重置协程
PT_EXIT(pt) // 退出协程

PT_SCHEDULE(f) // 调度一个协程,如果协程还在运行则返回值非0,如果协程退出则返回值为0
PT_YIELD(pt) // 主动出让协程
PT_YIELD_UNTIL(pt, cond) // 等待cond为真向下运行,否则出让当前协程

pt-sem.h 中几个数据接口和接口如下:

// 信号量数据结构
struct pt_sem {
  unsigned int count;
};

PT_SEM_INIT(s, c) // 初始化信号量值等于c
PT_SEM_WAIT(pt, s) // 等待信号量可用(>0),向下运行并消耗一个信号量
PT_SEM_SIGNAL(pt, s) // 给出一个信号量

使用演示

下面是个最简单的演示:
在这里插入图片描述

用上信号量的话上面代码可以改写成下面这样:

#include <stdio.h>
#include "pt-sem.h"

static time_t pretime = 0, nowstamp;

// 以下为信号量
static struct pt_sem sem1;
// 以下为协程控制数据
static struct pt pt1;
// 以下为协程函数
static PT_THREAD(protothread1(struct pt *pt)) {
  PT_BEGIN(pt); // 协程入口
  printf("Protothread1 begin\n\n");
  while(1) {
    PT_SEM_WAIT(pt, &sem1); // 等待信号量可用,并消耗信号量
    time(&nowstamp);
    printf("Protothread1 running, current time is %s\n", ctime(&nowstamp));
  }
  PT_END(pt); // 协程出口
}

int main(void) {
  time(&pretime);

  PT_INIT(&pt1); // 初始化协程控制数据结构
  PT_SEM_INIT(&sem1, 0); // 初始化信号量

  while(1) {
    protothread1(&pt1); // 运行协程

    // 以下代码每2s给出一个sem
    time(&nowstamp);
    if((nowstamp - pretime) >= 2) {
        pretime = nowstamp;
        PT_SEM_SIGNAL(&pt1, &sem1); // 给出信号量
    }
  }
}

在这里插入图片描述

下面是一个协程间调用的演示:

#include <stdio.h>
#include "pt.h"

static PT_THREAD(childpt(struct pt *pt)) {
  static int counter = 4; // 使用函数内部静态变量保存状态
  PT_BEGIN(pt); // 协程入口
  printf("childpt begin\n\n");

  while(counter--) {
    printf("childpt running, counter = %d\n\n", counter);
    PT_YIELD(pt); // 主动出让CPU
    printf("childpt resume run\n\n");
  }

  printf("childpt end\n\n");
  PT_END(pt); // 协程出口
}

static PT_THREAD(parentpt(struct pt *pt)) {
  static struct pt child;

  PT_BEGIN(pt); // 协程入口
  printf("parentpt begin\n\n");

  PT_SPAWN(pt, &child, childpt(&child)); // 调度子协程直至运行结束

  printf("parentpt end\n\n");
  PT_END(pt); // 协程出口
}

int main(void) {
  static struct pt parant;

  PT_INIT(&parant); // 初始化协程控制数据结构

  while(PT_SCHEDULE(parentpt(&parant))); // 调度父协程直至运行结束
  while(1);
}

在这里插入图片描述

总结

Protothread使用起来比较简单,当然功能也比较简单。另外使用时还有一定的限制,比如使用默认实现时不能在协程中使用 switch/case ,需要在协程中使用静态变量来保存相关数据等。

如果上了操作系统的话,Protothread这种协程相对来说意义一般,但是对于没有操作系统的单片机开发这些来说Protothread就非常好用了。

orchid是一个构建于强大的boost基础上的C ,类似于python下的gevent/eventlet,为用户提供基于协程的并发模型。 协程,顾名思义,协作式程序,其思想是,一系列互相依赖的协程间依次使用CPU,每次只有一个协程工作,而其他协程处于休眠状态。协程在控制离开时暂停执行,当控制再次进入时只能从离开的位置继续执行。 协程已经被证明是一种非常有用的程序组件,不仅被python、lua、ruby等脚本语言广泛采用,而且被新一代面向多核的编程语言如golang rust-lang等采用作为并发的基本单位。 协程可以被认为是一种用户空间线程,与传统的抢占式线程相比,有2个主要的优点: 与线程不同,协程是自己主动让出CPU,并交付他期望的下一个协程运行,而不是在任何时候都有可能被系统调度打断。因此协程使用更加清晰易懂,并且多数情况下不需要锁机制。 与线程相比,协程的切换由程序控制,发生在用户空间而非内核空间,因此切换的代价非常的小。 green化 术语“green化”来自于python下著名的协程greenlet,指改造IO对象以能和协程配合。某种意义上,协程线程的关系类似与线程与进程的关系,多个协程会在同一个线程的上下文之中运行。因此,当出现IO操作的时候,为了能够与协程相互配合,只阻塞当前协程而非整个线程,需要将io对象“green化”。目前orchid提供的green化的io对象包括: tcp socket(还不支持udp) descriptor(目前仅支持非文件类型文件描述符,如管道和标准输入/输出,文件类型的支持会在以后版本添加) timer (定时器) signal (信号) chan:协程间通信 chan这个概念引用自golang的chan。每个协程是一个独立的执行单元,为了能够方便协程之间的通信/同步,orchid提供了chan这种机制。chan本质上是一个阻塞消息队列,后面我们将看到,chan不仅可以用于同一个调度器上的协程之间的通信,而且可以用于不同调度器上的协程之间的通信。 多核 建议使用的scheduler per cpu的的模型来支持多核的机器,即为每个CPU核心分配一个调度器,有多少核心就创建多少个调度器。不同调度器的协程之间也可以通过chan来通信。协程应该被创建在哪个调度器里由用户自己决定。 进一步信息请阅读doc目录下tutorial。如果您发现任何bug或者有任何改进意见,请联系ioriiod0@gmail.com 标签:orchid
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Naisu Xu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值