如何创建多个运行线程?


59

有没有一种方法可以让程序的多个部分一起运行而无需在同一代码块中执行多项操作?

一个线程在等待外部设备,同时另一个线程中的LED闪烁。


3
您可能应该首先问自己是否真的需要线程。定时器可能已经可以满足您的需求,并且Arduino本身支持它们。
jfpoilpret 2014年

1
您可能也想签出Uzebox。这是一款两芯片的自制视频游戏机。因此,尽管它并非完全是Arduino,但整个系统都是基于中断构建的。因此,音频,视频,控件等都是中断驱动的,而主程序则不必担心其中的任何一个。可能是很好的参考。
cbmeeks 2015年

Answers:


50

Arduino上没有多进程或多线程支持。不过,您可以使用某些软件在接近多个线程的位置进行操作。

您想看一下Protothreads

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

当然,还有一个Arduino的例子在这里示例代码。这样的问题可能也很有用。

ArduinoThread也是一个不错的选择。


请注意,Arduino DUE对此有一个例外,它具有多个控制循环:arduino.cc/en/Tutorial/MultipleBlinks
tuskiomi

18

基于AVR的Arduino不支持(硬件)线程,我不熟悉基于ARM的Arduino。解决此限制的一种方法是使用中断,尤其是定时中断。您可以对计时器进行编程,使其每隔这么多微秒中断一次主例程,以运行特定的其他例程。

http://arduino.cc/en/Reference/Interrupts


15

可以在Uno上进行软件侧多线程处理。不支持硬件级线程。

为了实现多线程,将需要实现基本的调度程序并维护进程或任务列表以跟踪需要运行的不同任务。

一个非常简单的非抢占式调度程序的结构如下:

//Pseudocode
void loop()
{

for(i=o; i<n; i++) 
run(tasklist[i] for timelimit):

}

在这里,tasklist可以是一个函数指针数组。

tasklist [] = {function1, function2, function3, ...}

随着形式的每个功能:

int function1(long time_available)
{
   top:
   //Do short task
   if (run_time<time_available)
   goto top;
}

每个功能都可以执行单独的任务,例如function1执行LED操作和function2进行浮点计算。遵守分配给它的时间是每个任务(功能)的责任。

希望这足以让您入门。


2
我不确定使用非抢先式调度程序时是否会谈论“线程”。顺便说一句,这样的调度程序已经作为arduino库存在:arduino.cc/en/Reference/Scheduler
jfpoilpret 2014年

5
@jfpoilpret-协作多线程是真实的事情。
康纳·沃尔夫

你是对的!我的错; 很久以前,我还没有遇到过协作式多线程,因此在我看来,多线程必须具有先发制人性。
jfpoilpret 2014年

9

根据您的要求的描述:

  • 一个线程在等待外部设备
  • 一线闪烁一个LED

看来您可以在第一个“线程”中使用一个Arduino中断(实际上,我宁愿称其为“任务”)。

Arduino中断可以根据外部事件(数字输入引脚上的电压电平或电平变化)调用一个功能(您的代码),这将立即触发您的功能。

但是,对于中断要记住的重要一点是,被调用函数应尽可能快(通常,不应存在任何delay()调用或依赖的任何其他API delay())。

如果您有一个很长的任务要在外部事件触发时激活,那么您可能会使用协作调度程序,并从中断函数向其添加新任务。

关于中断的第二个重要点是中断的数量是有限的(例如,UNO上只有2个)。因此,如果您开始有更多的外部事件,则需要实现将所有输入多路复用为一个的某种方式,并让中断功能确定实际触发了哪些多路复用输入。


6

一个简单的解决方案是使用Scheduler。有几种实现。简要介绍了一种适用于基于AVR和SAM的板。基本上,一个电话将启动一个任务。“在草图中素描”。

#include <Scheduler.h>
....
void setup()
{
  ...
  Scheduler.start(taskSetup, taskLoop);
}

Scheduler.start()将添加一个新任务,该任务将运行一次taskSetup,然后在Arduino草图工作时重复调用taskLoop。该任务具有其自己的堆栈。堆栈的大小是可选参数。默认堆栈大小为128字节。

为了允许上下文切换,任务需要调用yield()delay()。还有一个用于等待条件的支持宏。

await(Serial.available());

该宏是以下语法糖:

while (!(Serial.available())) yield();

等待也可以用于同步任务。以下是一个示例片段:

volatile int taskEvent = 0;
#define signal(evt) do { await(taskEvent == 0); taskEvent = evt; } while (0)
...
void taskLoop()
{
  await(taskEvent);
  switch (taskEvent) {
  case 1: 
  ...
  }
  taskEvent = 0;
}
...
void loop()
{
  ...
  signal(1);
}

有关更多详细信息,请参见示例。有多个LED闪烁到反跳按钮的示例,以及一个具有非阻塞命令行读取功能的简单外壳。模板和名称空间可用于帮助结构化和减少源代码。下显示了如何使用模板功能进行多次闪烁。堆栈只有64个字节就足够了。

#include <Scheduler.h>

template<int pin> void setupBlink()
{
  pinMode(pin, OUTPUT);
}

template<int pin, unsigned int ms> void loopBlink()
{
  digitalWrite(pin, HIGH);
  delay(ms);
  digitalWrite(pin, LOW);
  delay(ms);
}

void setup()
{
  Scheduler.start(setupBlink<11>, loopBlink<11,500>, 64);
  Scheduler.start(setupBlink<12>, loopBlink<12,250>, 64);
  Scheduler.start(setupBlink<13>, loopBlink<13,1000>, 64);
}

void loop()
{
  yield();
}

还有一个基准可以让您对性能有所了解,例如开始任务的时间,上下文切换等。

最后,有一些支持类用于任务级同步和通信。队列信号量


3

从该论坛的前一个话题开始,以下问题/答案移至了电气工程。它具有示例arduino代码,可在使用主循环执行串行IO时使用计时器中断使LED闪烁。

https://electronics.stackexchange.com/questions/67089/how-can-i-control-things-without-using-delay/67091#67091

重新发布:

中断是在其他情况发生时完成工作的一种常用方法。在下面的示例中,不使用时,LED闪烁delay()。每当Timer1触发时,isrBlinker()就会调用中断服务程序(ISR)。它打开/关闭LED。

为了表明其他事情可以同时发生,请loop()反复将foo / bar写入串行端口,而与LED闪烁无关。

#include "TimerOne.h"

int led = 13;

void isrBlinker()
{
  static bool on = false;
  digitalWrite( led, on ? HIGH : LOW );
  on = !on;
}

void setup() {                
  Serial.begin(9600);
  Serial.flush();
  Serial.println("Serial initialized");

  pinMode(led, OUTPUT);

  // initialize the ISR blinker
  Timer1.initialize(1000000);
  Timer1.attachInterrupt( isrBlinker );
}

void loop() {
  Serial.println("foo");
  delay(1000);
  Serial.println("bar");
  delay(1000);
}

这是一个非常简单的演示。ISR可能更为复杂,并且可以由计时器和外部事件(引脚)触发。许多通用库都是使用ISR实现的。



2

您也可以尝试一下我的ThreadHandler库

https://bitbucket.org/adamb3_14/threadhandler/src/master/

它使用中断调度程序来允许上下文切换,而无需中继yield()或delay()。

我创建该库是因为我需要三个线程,无论其他线程在做什么,我都需要两个线程在准确的时间运行。第一个线程处理串行通信。第二个是使用带有Eigen库的浮点矩阵乘法来运行Kalman滤波器。第三个是快速电流控制回路线程,该线程必须能够中断矩阵计算。

这个怎么运作

每个循环线程都有一个优先级和一个周期。如果优先级比当前执行线程高的线程到达其下一个执行时间,调度程序将暂停当前线程并切换到优先级更高的线程。一旦高优先级线程完成其执行,调度程序就会切换回上一个线程。

排程规则

ThreadHandler库的调度方案如下:

  1. 最高优先级优先。
  2. 如果优先级相同,则首先执行截止时间最早的线程。
  3. 如果两个线程的截止日期相同,则第一个创建的线程将首先执行。
  4. 线程只能被优先级更高的线程中断。
  5. 一旦线程执行完毕,它将以较低的优先级阻止所有线程的执行,直到run函数返回为止。
  6. 与ThreadHandler线程相比,循环功能的优先级为-128。

如何使用

可以通过c ++继承创建线程

class MyThread : public Thread
{
public:
    MyThread() : Thread(priority, period, offset){}

    virtual ~MyThread(){}

    virtual void run()
    {
        //code to run
    }
};

MyThread* threadObj = new MyThread();

或通过createThread和lambda函数

Thread* myThread = createThread(priority, period, offset,
    []()
    {
        //code to run
    });

创建线程对象时,它们会自动连接到ThreadHandler。

要开始执行创建的线程对象,请调用:

ThreadHandler::getInstance()->enableThreadExecution();

1

这是另一个微处理器协作式多任务库– PQRST:运行简单任务的优先级队列。

在此模型中,线程被实现为的子类Task,该子类被安排在将来的某个时间进行(并且有可能按规则的时间间隔进行重新安排,如果通常LoopTask改为子类)。run()任务到期时将调用对象的方法。该run()方法做了一些应做的工作,然后返回(这是协作位);它通常会维护某种状态机来管理其对连续调用的操作(一个简单的示例是light_on_p_下面示例中的变量)。它需要重新考虑如何组织代码,但是事实证明,在相当密集的使用中,它非常灵活和健壮。

它与时间单位无关,因此millis()micros()或任何其他方便的刻度为单位运行时就很高兴。

这是使用此库实现的“闪烁”程序。这仅显示了一个正在运行的任务:通常会创建其他任务,然后在中启动其他任务setup()

#include "pqrst.h"

class BlinkTask : public LoopTask {
private:
    int my_pin_;
    bool light_on_p_;
public:
    BlinkTask(int pin, ms_t cadence);
    void run(ms_t) override;
};

BlinkTask::BlinkTask(int pin, ms_t cadence)
    : LoopTask(cadence),
      my_pin_(pin),
      light_on_p_(false)
{
    // empty
}
void BlinkTask::run(ms_t t)
{
    // toggle the LED state every time we are called
    light_on_p_ = !light_on_p_;
    digitalWrite(my_pin_, light_on_p_);
}

// flash the built-in LED at a 500ms cadence
BlinkTask flasher(LED_BUILTIN, 500);

void setup()
{
    pinMode(LED_BUILTIN, OUTPUT);
    flasher.start(2000);  // start after 2000ms (=2s)
}

void loop()
{
    Queue.run_ready(millis());
}

这些是“运行完成”任务,对吗?
Edgar Bonet

@EdgarBonet我不确定您的意思。run()调用该方法后,它不会被中断,因此它有责任合理迅速地完成操作。不过,通常情况下,它会先进行工作,然后重新安排自己的时间(如果是的子类,可能会自动重新安排LoopTask时间),以便将来使用。一个常见的模式是任务维护一些内部状态机(一个简单的例子是light_on_p_上面的状态),以便它在下一次到期时表现适当。
诺曼·格雷

所以是的,这些都是运行到完成(RtC)任务:在当前任务通过从返回来完成其执行之前,没有任务可以运行run()。这与协作线程相反,协作线程可以通过调用yield()或产生CPU delay()。或抢占式线程,可以随时调度。我觉得区别很重要,因为我已经看到很多来这里搜索线程的人这样做是因为他们更喜欢编写阻塞代码而不是状态机。阻塞产生CPU的实际线程是可以的。否阻止RtC任务。
Edgar Bonet

@EdgarBonet这是一个有用的区别,是的。我将这种风格的线程和yield风格的线程都看作是与合作式线程不同的协作线程,这与抢占式线程相反,但是的确,它们需要不同的编码方式。仔细地,深入地比较这里提到的各种方法将是很有趣的。上面没有提到的一个不错的库是protothreads。我既要批评也要赞美。我(当然)更喜欢我的方法,因为它看起来是最明确的,不需要额外的堆栈。
诺曼·格雷

(更正:protothreads 中提到,@ sachleen的答案
诺曼灰色
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.