Как создать несколько запущенных потоков?

Есть ли способ, при котором несколько частей программы могут работать вместе, не выполняя несколько операций в одном блоке кода?

Один поток ожидает внешнего устройства, а в другом потоке мигает светодиод.

, 👍81

Обсуждение

Вероятно, вам следует сначала спросить себя, действительно ли вам нужны потоки. Таймеры могут уже подойти для ваших нужд, и они изначально поддерживаются в Arduino., @jfpoilpret

Вы также можете проверить Uzebox. Это двухчиповая домашняя игровая приставка. Так что, хотя это и не совсем Arduino, вся система построена на прерываниях. Таким образом, аудио, видео, элементы управления и т. д. управляются прерываниями, в то время как основной программе не нужно ни о чем беспокоиться. Может быть хорошей ссылкой., @cbmeeks

Используйте библиотеку Arduino [NonBlockingSequence](https://github.com/AhmedYousryM/NonBlockingSequence). Вы можете определить более одной последовательности. Каждая последовательность может описывать конкретный поток., @user1774936


9 ответов


Лучший ответ:

62

В Arduino нет поддержки нескольких процессов или многопоточности. Однако вы можете сделать что-то близкое к нескольким потокам с помощью некоторого программного обеспечения.

Вы хотите ознакомиться с Protothreads:

Протопотоки – это чрезвычайно легкие бесстековые потоки, предназначенные для системы с жестким ограничением памяти, такие как небольшие встроенные системы или узлы беспроводной сенсорной сети. Протопотоки обеспечивают линейный код выполнения для управляемых событиями систем, реализованных на C. Протопотоки могут использоваться с базовой операционной системой или без нее для обеспечения блокирующие обработчики событий. Протопотоки обеспечивают последовательный поток управление без сложных конечных автоматов или полной многопоточности.

Конечно, есть пример Arduino здесь с пример кода. Этот SO-вопрос тоже может быть полезен.

ArduinoThread тоже подойдет.

,

Обратите внимание, что Arduino DUE имеет исключение из этого правила с несколькими контурами управления: https://www.arduino.cc/en/Tutorial/MultipleBlinks., @tuskiomi


19

На Uno можно выполнять многопоточность на стороне программного обеспечения. Потоки на аппаратном уровне не поддерживаются.

Для достижения многопоточности потребуется реализация базового планировщика и ведение списка процессов или задач для отслеживания различных задач, которые необходимо выполнить.

Структура очень простого планировщика без вытеснения будет выглядеть следующим образом:

//Псевдокод
void loop()
{

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

}

Здесь tasklist может быть массивом указателей на функции.

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

С каждой функцией формы:

int function1(long time_available)
{
   top:
   //Выполнить короткую задачу
   if (run_time<time_available)
   goto top;
}

Каждая функция может выполнять отдельную задачу, например, function1 выполняет манипуляции со светодиодами, а function2 выполняет вычисления с плавающей запятой. Каждая задача (функция) будет нести ответственность за соблюдение отведенного на нее времени.

Надеюсь, этого будет достаточно для начала.

,

Я не уверен, что стал бы говорить о «потоках» при использовании планировщика без вытеснения. Кстати, такой планировщик уже существует в виде библиотеки arduino: http://arduino.cc/en/Reference/Scheduler, @jfpoilpret

@jfpoilpret - Совместная многопоточность - это реальная вещь., @Connor Wolf

Да, ты прав! Виноват; это было так давно, что я не сталкивался с совместной многопоточностью, и, по моему мнению, многопоточность должна была быть вытесняющей., @jfpoilpret


21

Arduino на базе AVR не поддерживает (аппаратную) многопоточность, я не знаком с Arduino на базе ARM. Одним из способов обойти это ограничение является использование прерываний, особенно прерываний по времени. Вы можете запрограммировать таймер так, чтобы он прерывал основную процедуру каждые несколько микросекунд, чтобы запустить определенную другую процедуру.

https://www.arduino.cc/reference/en/language/functions/ прерывания/прерывания/

,

9

В соответствии с описанием ваших требований:

  • один поток ожидает внешнего устройства
  • один поток мигает светодиодом

Кажется, вы могли бы использовать одно прерывание Arduino для первого «потока» (на самом деле я бы скорее назвал его «задачей»).

Прерывания Arduino могут вызывать одну функцию (ваш код) на основе внешнего события (уровень напряжения или изменение уровня на цифровом входном контакте), что немедленно вызовет вашу функцию.

Однако при работе с прерываниями следует помнить об одном важном моменте: вызываемая функция должна выполняться как можно быстрее (как правило, не должно быть вызова delay() или любого другого API, от которого зависит при задержке()).

Если у вас есть длительная задача, которую нужно активировать при внешнем срабатывании, вы можете потенциально использовать совместный планировщик и добавить к ней новую задачу из функции прерывания.

Второй важный момент, связанный с прерываниями, заключается в том, что их количество ограничено (например, только 2 в UNO). Так что, если вы начинаете получать больше внешних событий, вам нужно реализовать своего рода мультиплексирование всех входов в один, и ваша функция прерывания должна определять, какой мультиплексированный вход был фактическим триггером.

,

3

Из предыдущего заклинания этого форума следующий вопрос/ответ был перенесен в Электротехнику. В нем есть пример кода Arduino для мигания светодиодом с использованием прерывания таймера при использовании основного цикла для последовательного ввода-вывода.

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

Репост:

Прерывание — это распространенный способ сделать что-то, пока что-то происходит. В приведенном ниже примере светодиод мигает без использования delay(). Всякий раз, когда срабатывает Timer1, вызывается подпрограмма обслуживания прерываний (ISR) isrBlinker(). Включает/выключает светодиод.

Чтобы показать, что одновременно могут происходить и другие вещи, loop() многократно записывает foo/bar в последовательный порт независимо от мигания светодиода.

#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);

  // инициализируем мигалку ISR
  Timer1.initialize(1000000);
  Timer1.attachInterrupt( isrBlinker );
}

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

Это очень простая демонстрация. ISR могут быть гораздо более сложными и могут запускаться таймерами и внешними событиями (выводами). Многие распространенные библиотеки реализованы с использованием ISR.

,

5

Я также пришел к этой теме при реализации матричного светодиодного дисплея.

Одним словом, вы можете создать планировщик опроса, используя функцию millis() и прерывание таймера в Arduino.

Я предлагаю следующие статьи Билла Эрла:

https://learn.adafruit.com/multi-tasking-the-arduino -part-1/обзор

https://learn.adafruit.com/multi-tasking-the-arduino -part-2/обзор

https://learn.adafruit.com/multi-tasking-the-arduino -part-3/обзор

,

8

Простым решением является использование планировщика. Есть несколько реализаций. Здесь кратко описывается тот, который доступен для плат на базе AVR и SAM. В основном один вызов запустит задачу; "скетч в скетче".

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

Scheduler.start() добавит новую задачу, которая запустит taskSetup один раз, а затем повторно вызовет taskLoop, как работает скетч Arduino. Задача имеет собственный стек. Размер стека является необязательным параметром. Размер стека по умолчанию составляет 128 байт.

Чтобы разрешить переключение контекста, задачи должны вызывать yield() или delay(). Также есть макрос поддержки для ожидания условия.

await(Serial.available());

Макрос является синтаксическим сахаром для следующего:

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

Await также можно использовать для синхронизации задач. Ниже приведен пример фрагмента:

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);
}

Подробнее см. в примерах. Есть примеры от многократного мигания светодиода до кнопки устранения дребезга и простой оболочки с неблокирующим чтением командной строки. Шаблоны и пространства имен можно использовать для структурирования и сокращения исходного кода. Ниже схема показано, как использовать шаблонные функции для многократного моргания. Для стека достаточно 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

Вы также можете попробовать мою библиотеку ThreadHandler

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

Он использует планировщик прерываний, чтобы разрешить переключение контекста без ретрансляции на yield() или delay().

Я создал библиотеку, потому что мне нужно было три потока, и мне нужно было, чтобы два из них запускались в точное время независимо от того, что делали другие. Первый поток обрабатывал последовательную связь. Второй запускал фильтр Калмана, используя умножение матрицы с плавающей запятой с библиотекой Eigen. И третий — поток быстрого цикла управления током, который должен был иметь возможность прерывать матричные вычисления.

Как это работает

Каждый циклический поток имеет приоритет и период. Если поток с более высоким приоритетом, чем текущий исполняемый поток, достигает своего следующего времени выполнения, планировщик приостанавливает текущий поток и переключается на поток с более высоким приоритетом. Как только поток с высоким приоритетом завершает свое выполнение, планировщик переключается обратно на предыдущий поток.

Правила планирования

Схема планирования библиотеки ThreadHandler выглядит следующим образом:

  1. Сначала наивысший приоритет.
  2. Если приоритет тот же, то поток с самым ранним сроком выполнения выполняется первым.
  3. Если два потока имеют одинаковый крайний срок, первый созданный поток будет выполняться первым.
  4. Поток может быть прерван только потоками с более высоким приоритетом.
  5. После выполнения потока он блокирует выполнение для всех потоков с более низким приоритетом до тех пор, пока функция запуска не вернется.
  6. Функция цикла имеет приоритет -128 по сравнению с потоками ThreadHandler.

Как использовать

Потоки можно создавать с помощью наследования C++

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

    virtual ~MyThread(){}

    virtual void run()
    {
        //код для запуска
    }
};

MyThread* threadObj = new MyThread();

Или через createThread и лямбда-функцию

Thread* myThread = createThread(priority, period, offset,
    []()
    {
        //код для запуска
    });

Объекты потоков автоматически подключаются к ThreadHandler при их создании.

Чтобы начать выполнение созданного вызова объектов потока:

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

2

А вот еще одна многозадачная библиотека для совместного использования микропроцессоров — PQRST: приоритетная очередь для выполнения простых задач.

  • Главная страница
  • Документация на уровне класса
  • Репозиторий с загрузки и список проблем

В этой модели поток реализуется как подкласс Task, который запланирован на какое-то время в будущем (и, возможно, перепланируется через регулярные промежутки времени, если, как это обычно бывает, он является подклассом LoopTask). Метод объекта run() вызывается при наступлении срока выполнения задачи. Метод run() выполняет определенную работу, а затем возвращает результат (это кооперативный бит); обычно он поддерживает своего рода конечный автомат для управления своими действиями при последовательных вызовах (тривиальным примером является переменная light_on_p_ в приведенном ниже примере). Это требует небольшого переосмысления того, как вы организуете свой код, но оказалось очень гибким и надежным при довольно интенсивном использовании.

Он не зависит от единиц времени, поэтому его можно использовать как в единицах millis(), так и в micros() или любых других удобных тиках.

Вот программа 'blink', реализованная с использованием этой библиотеки. Это показывает только одну запущенную задачу: другие задачи обычно создаются и запускаются в 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)
{
    // пустой
}
void BlinkTask::run(ms_t t)
{
    // переключаем состояние светодиода каждый раз, когда нас вызывают
    light_on_p_ = !light_on_p_;
    digitalWrite(my_pin_, light_on_p_);
}

// мигать встроенным светодиодом с частотой 500 мс
BlinkTask flasher(LED_BUILTIN, 500);

void setup()
{
    pinMode(LED_BUILTIN, OUTPUT);
    flasher.start(2000);  // запуск через 2000 мс (= 2 с)
}

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

Это задачи «выполнить до завершения», верно?, @Edgar Bonet

@EdgarBonet Я не совсем понимаю, что вы имеете в виду. После вызова метода run() он не прерывается, поэтому он должен завершить работу достаточно быстро. Однако обычно он делает свою работу, а затем переназначает себя (возможно, автоматически, в случае подкласса LoopTask) на некоторое время в будущем. Обычный шаблон для задачи состоит в том, чтобы поддерживать некоторый внутренний конечный автомат (тривиальный пример — состояние light_on_p_ выше), чтобы он вел себя соответствующим образом, когда наступит следующий срок., @Norman Gray

Так что да, это задачи, выполняемые до завершения (RtC): ни одна задача не может быть запущена до тех пор, пока текущая не завершит свое выполнение, вернувшись из run(). Это отличается от кооперативных потоков, которые могут отдавать ЦП, например, вызывая yield() или delay(). Или вытесняющие потоки, которые могут быть запланированы в любое время. Я чувствую, что это различие важно, поскольку я видел, что многие люди, которые приходят сюда в поисках потоков, делают это, потому что предпочитают писать блокирующий код, а не конечные автоматы. Блокировка реальных потоков, которые отдают ЦП, — это нормально. Блокировка задач RtC — нет., @Edgar Bonet

@EdgarBonet Да, это полезное различие. Я бы рассматривал и этот стиль, и потоки в стиле yield просто как разные стили кооперативного потока, в отличие от потоков с вытеснением, но это правда, что они требуют другого подхода к их кодированию. Было бы интересно увидеть вдумчивое и глубокое сравнение различных подходов, упомянутых здесь; одна хорошая библиотека, не упомянутая выше, — это [protothreads](http://dunkels.com/adam/pt/). В обоих я нахожу за что критиковать, но и за похвалу. Я (конечно) предпочитаю свой подход, потому что он кажется наиболее явным и не требует дополнительных стеков., @Norman Gray

(исправление: протопотоки _были_ упомянуты в @sachleen answer), @Norman Gray