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

60

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

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

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

Ответы:

50

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

Вы хотите посмотреть на Protothreads :

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

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

ArduinoThread тоже хороший.

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

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

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

jippie
источник
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выполнение манипуляций со светодиодами и function2выполнение вычислений с плавающей запятой. Каждое задание (функция) будет нести ответственность за соблюдение выделенного ему времени.

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

asheeshr
источник
2
Я не уверен, что буду говорить о «потоках» при использовании не вытесняющего планировщика. Кстати, такой планировщик уже существует в виде библиотеки
arduino
5
@jfpoilpret - Совместная многопоточность - реальная вещь.
Коннор Вольф
Да, ты прав! Виноват; это было так давно, что я не сталкивался с совместной многопоточностью, что, по моему мнению, многопоточность должна быть преимущественной.
jfpoilpret
9

Согласно описанию ваших требований:

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

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

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

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

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

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

jfpoilpret
источник
6

Простое решение - использовать планировщик . Есть несколько реализаций. Это вкратце описывает тот, который доступен для плат на базе 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

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

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

Repost:

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

Чтобы показать, что одновременно могут происходить другие вещи, 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);

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

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

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

walrii
источник
3

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

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

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

https://learn.adafruit.com/multi-tasking-the-arduino-part-1/overview

https://learn.adafruit.com/multi-tasking-the-arduino-part-2/overview

https://learn.adafruit.com/multi-tasking-the-arduino-part-3/overview

intelliarm
источник
2

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

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

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

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

Как это устроено

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

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

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

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

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

Потоки могут быть созданы с помощью наследования c ++

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

    virtual ~MyThread(){}

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

MyThread* threadObj = new MyThread();

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

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

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

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

ThreadHandler::getInstance()->enableThreadExecution();
Адам Бэкстрем
источник
1

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

В этой модели поток реализован как подкласс a 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)
{
    // 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());
}
Норман грей
источник
Это задачи «от завершения к завершению», верно?
Эдгар Бонет
@ EdgarBonet Я не совсем понимаю, что вы имеете в виду. После того, как run()метод вызван, он не прерывается, поэтому он обязан быстро завершить работу. Как правило, однако, он выполнит свою работу, а затем перенесет себя (возможно, автоматически, в случае подкласса LoopTask) на некоторое время в будущем. Обычным шаблоном для задачи является поддержание некоторого внутреннего конечного автомата (тривиальным примером является light_on_p_состояние выше), чтобы он вел себя соответствующим образом, когда наступит следующий срок.
Норман Грей
Так что да, это задачи запуска до завершения (RtC): ни одна задача не может быть запущена, пока текущая не завершит свое выполнение, вернувшись из run(). Это отличается от кооперативных потоков, которые могут выдавать ЦП, например, путем вызова yield()или delay(). Или приоритетные потоки, которые могут быть запланированы в любое время. Я чувствую, что различие важно, так как я видел, что многие люди, которые приходят сюда в поисках потоков, делают это, потому что они предпочитают писать блокирующий код, а не конечные автоматы. Блокировка реальных потоков, которые дают процессор, в порядке. Блокировки задач RtC нет.
Эдгар Бонет
@ EdgarBonet Это полезное различие, да. Я бы рассматривал как этот стиль, так и потоки в стиле yield, как просто разные стили кооперативных потоков, в отличие от вытесняющих потоков, но это правда, что они требуют другого подхода к их кодированию. Было бы интересно увидеть вдумчивое и всестороннее сравнение различных подходов, упомянутых здесь; одна хорошая библиотека, не упомянутая выше, это protothreads . Я нахожу вещи для критики в обоих, но и для похвалы. Я (конечно) предпочитаю свой подход, потому что он кажется наиболее явным и не требует дополнительных стеков.
Норман Грей
(коррекция: protothreads было упомянуто, в @ sachleen Ответим )
Norman Gray