Wie kann ich mehrere laufende Threads erstellen?


59

Gibt es eine Möglichkeit, mehrere Teile des Programms zusammen laufen zu lassen, ohne mehrere Dinge im selben Codeblock zu tun?

Ein Thread wartet auf ein externes Gerät, während gleichzeitig eine LED in einem anderen Thread blinkt.


3
Sie sollten sich wahrscheinlich zuerst fragen, ob Sie wirklich Themen benötigen. Timer sind für Ihre Anforderungen möglicherweise bereits in Ordnung und werden von Arduino nativ unterstützt.
jfpoilpret

1
Vielleicht möchten Sie auch die Uzebox auschecken. Es ist eine Homebrew-Videospielkonsole mit zwei Chips. Also, während es nicht gerade ein Arduino ist, ist das gesamte System auf Interrupts aufgebaut. So werden Audio, Video, Steuerelemente usw. alle durch Interrupts gesteuert, während sich das Hauptprogramm nicht darum kümmern muss. Kann eine gute Referenz sein.
cbmeeks

Antworten:


50

Auf dem Arduino gibt es keine Unterstützung für mehrere Prozesse und kein Multi-Threading. Mit einiger Software können Sie jedoch nahezu mehrere Threads ausführen.

Sie möchten sich Protothreads ansehen :

Protothreads sind extrem leichte stapellose Threads, die für Systeme mit stark eingeschränktem Arbeitsspeicher entwickelt wurden, z. B. kleine eingebettete Systeme oder drahtlose Sensornetzwerkknoten. Protothreads bieten eine lineare Codeausführung für ereignisgesteuerte Systeme, die in C implementiert sind. Protothreads können mit oder ohne zugrunde liegendes Betriebssystem verwendet werden, um blockierende Ereignishandler bereitzustellen. Protothreads ermöglichen einen sequenziellen Steuerungsfluss ohne komplexe Zustandsmaschinen oder vollständiges Multithreading.

Natürlich gibt es ein Arduino Beispiel hier mit Beispielcode . Diese SO-Frage könnte auch nützlich sein.

ArduinoThread ist auch gut.


Beachten Sie, dass der Arduino DUE eine Ausnahme mit mehreren Regelkreisen hat: arduino.cc/en/Tutorial/MultipleBlinks
tuskiomi

18

AVR-basierte Arduinos unterstützen kein (Hardware-) Threading. Ich bin mit ARM-basierten Arduinos nicht vertraut. Ein Weg, um diese Einschränkung zu umgehen, ist die Verwendung von Interrupts, insbesondere von zeitgesteuerten Interrupts. Sie können einen Timer programmieren, um die Hauptroutine alle so viele Mikrosekunden zu unterbrechen und eine bestimmte andere Routine auszuführen.

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


15

Auf dem Uno ist softwareseitiges Multithreading möglich. Hardware Level Threading wird nicht unterstützt.

Um Multithreading zu erreichen, ist die Implementierung eines grundlegenden Schedulers und die Verwaltung eines Prozesses oder einer Aufgabenliste erforderlich, um die verschiedenen auszuführenden Aufgaben zu verfolgen.

Die Struktur eines sehr einfachen nicht präemptiven Schedulers sieht folgendermaßen aus:

//Pseudocode
void loop()
{

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

}

Hierbei tasklistkann es sich um ein Array von Funktionszeigern handeln.

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

Mit jeder Funktion des Formulars:

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

Jede Funktion kann eine separate Aufgabe function1ausführen, z. B. LED-Manipulationen und function2Float-Berechnungen. Es liegt in der Verantwortung jeder Aufgabe (Funktion), die ihr zugewiesene Zeit einzuhalten.

Hoffentlich sollte dies ausreichen, um Ihnen den Einstieg zu erleichtern.


2
Ich bin nicht sicher, ob ich über "Threads" sprechen würde, wenn ich einen nicht präemptiven Scheduler verwende. Übrigens gibt es einen solchen Scheduler bereits als Arduino-Bibliothek: arduino.cc/en/Reference/Scheduler
jfpoilpret

5
@jfpoilpret - Kooperatives Multithreading ist eine echte Sache.
Connor Wolf

Ja, du hast Recht! Mein Fehler; Es war so lange her, dass ich nicht mit kooperativem Multithreading konfrontiert war, und in meinen Augen musste Multithreading präventiv sein.
jfpoilpret

9

Nach der Beschreibung Ihrer Anforderungen:

  • Ein Thread wartet auf ein externes Gerät
  • ein Thread blinkt eine LED

Es scheint, dass Sie einen Arduino-Interrupt für den ersten "Thread" verwenden könnten (ich würde es eher als "Task" bezeichnen).

Arduino-Interrupts können eine Funktion (Ihren Code) basierend auf einem externen Ereignis (Spannungspegel oder Pegeländerung an einem digitalen Eingangspin) aufrufen, die Ihre Funktion sofort auslöst.

Ein wichtiger Punkt bei Interrupts ist jedoch, dass die aufgerufene Funktion so schnell wie möglich ist (normalerweise sollte es keinen delay()Aufruf oder eine andere API geben, die davon abhängt delay()).

Wenn Sie eine lange Aufgabe haben, die beim Auslösen eines externen Ereignisses aktiviert werden muss, können Sie möglicherweise einen kooperativen Scheduler verwenden und dieser von Ihrer Interrupt-Funktion aus eine neue Aufgabe hinzufügen.

Ein zweiter wichtiger Punkt bei Interrupts ist, dass ihre Anzahl begrenzt ist (z. B. nur 2 bei UNO). Wenn Sie also mehr externe Ereignisse haben möchten, müssen Sie eine Art Multiplexing aller Eingänge in einen implementieren und Ihre Interrupt-Funktion bestimmen lassen, welcher Multiplexing-Eingang der eigentliche Auslöser war.


6

Eine einfache Lösung ist die Verwendung eines Schedulers . Es gibt mehrere Implementierungen. Dies beschreibt in Kürze eine, die für AVR- und SAM-basierte Karten verfügbar ist. Grundsätzlich startet ein einzelner Anruf eine Aufgabe. msgstr "innerhalb einer Skizze skizzieren".

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

Scheduler.start () fügt eine neue Aufgabe hinzu, die das taskSetup einmal ausführt und dann wiederholt taskLoop aufruft, genau wie die Arduino-Skizze funktioniert. Die Aufgabe hat einen eigenen Stapel. Die Größe des Stapels ist ein optionaler Parameter. Die Standardstapelgröße beträgt 128 Byte.

Um Kontextwechsel zu ermöglichen, müssen die Tasks yield () oder delay () aufrufen . Es gibt auch ein Unterstützungsmakro für das Warten auf eine Bedingung.

await(Serial.available());

Das Makro ist syntaktischer Zucker für Folgendes:

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

Warten kann auch zum Synchronisieren von Aufgaben verwendet werden. Unten ist ein Beispiel-Snippet:

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

Weitere Details finden Sie in den Beispielen . Es gibt Beispiele für das Blinken mehrerer LEDs, um die Schaltfläche zu entprellen, und eine einfache Shell mit nicht blockierendem Befehlszeilenlesevorgang. Vorlagen und Namespaces können zum Strukturieren und Reduzieren des Quellcodes verwendet werden. Die folgende Skizze zeigt, wie Vorlagenfunktionen für Multi-Blink verwendet werden. Es reicht mit 64 Bytes für den Stack.

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

Es gibt auch einen Benchmark , der eine Vorstellung von der Leistung gibt, dh Zeit zum Starten der Aufgabe, Kontextwechsel usw.

Zuletzt gibt es einige Support-Klassen für die Synchronisierung und Kommunikation auf Task-Ebene. Warteschlange und Semaphor .


3

Aus einer früheren Beschwörung dieses Forums wurde die folgende Frage / Antwort nach Elektrotechnik verschoben. Es hat Beispiel-Arduino-Code, um eine LED mit einem Timer-Interrupt zu blinken, während die Hauptschleife für serielle E / A verwendet wird.

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

Repost:

Interrupts sind eine übliche Methode, um Dinge zu erledigen, während etwas anderes läuft. Im folgenden Beispiel blinkt die LED ohne Verwendung von delay(). Bei jedem Timer1Brand wird die Interrupt Service Routine (ISR) isrBlinker()aufgerufen. Es schaltet die LED ein / aus.

Um zu zeigen, dass andere Dinge gleichzeitig passieren können, wird loop()wiederholt foo / bar auf die serielle Schnittstelle geschrieben, unabhängig davon , ob die LED blinkt.

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

Dies ist eine sehr einfache Demo. ISRs können sehr viel komplexer sein und durch Timer und externe Ereignisse (Pins) ausgelöst werden. Viele der gängigen Bibliotheken werden mithilfe von ISRs implementiert.


3

Ich bin auch auf dieses Thema gekommen, als ich eine Matrix-LED-Anzeige implementiert habe.

Mit einem Wort, Sie können einen Abrufplaner erstellen, indem Sie in Arduino die Funktion millis () und den Timer-Interrupt verwenden.

Ich schlage folgende Artikel von Bill Earl vor:

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


2

Sie könnten auch meine ThreadHandler-Bibliothek ausprobieren

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

Es verwendet einen unterbrechenden Scheduler, um das Umschalten des Kontexts zu ermöglichen, ohne auf yield () oder delay () zu verweisen.

Ich habe die Bibliothek erstellt, weil ich drei Threads brauchte und zwei von ihnen, um genau zu laufen, egal was die anderen taten. Der erste Thread behandelte die serielle Kommunikation. Im zweiten Beispiel wurde ein Kalman-Filter mit Float-Matrix-Multiplikation mit der Eigen-Bibliothek ausgeführt. Und der dritte war ein schneller Stromregelkreis-Thread, der in der Lage sein musste, die Matrixberechnungen zu unterbrechen.

Wie es funktioniert

Jeder zyklische Thread hat eine Priorität und eine Periode. Wenn ein Thread mit höherer Priorität als der aktuell ausgeführte Thread seine nächste Ausführungszeit erreicht, hält der Scheduler den aktuellen Thread an und wechselt zu dem Thread mit der höheren Priorität. Sobald der Thread mit hoher Priorität seine Ausführung abgeschlossen hat, wechselt der Scheduler zurück zum vorherigen Thread.

Planungsregeln

Das Planungsschema der ThreadHandler-Bibliothek lautet wie folgt:

  1. Höchste Priorität zuerst.
  2. Wenn die Priorität gleich ist, wird der Thread mit der frühesten Frist zuerst ausgeführt.
  3. Wenn zwei Threads dieselbe Frist haben, wird der zuerst erstellte Thread zuerst ausgeführt.
  4. Ein Thread kann nur von Threads mit höherer Priorität unterbrochen werden.
  5. Sobald ein Thread ausgeführt wird, wird die Ausführung für alle Threads mit niedrigerer Priorität blockiert, bis die Ausführungsfunktion zurückkehrt.
  6. Die Schleifenfunktion hat im Vergleich zu ThreadHandler-Threads die Priorität -128.

Wie benutzt man

Threads können über C ++ - Vererbung erstellt werden

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

    virtual ~MyThread(){}

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

MyThread* threadObj = new MyThread();

Oder über createThread und eine Lambda-Funktion

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

Thread-Objekte stellen beim Erstellen automatisch eine Verbindung zum ThreadHandler her.

So starten Sie die Ausführung von erstellten Thread-Objekten:

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

1

Und hier ist noch eine weitere kooperative Mikroprozessor-Multitasking-Bibliothek - PQRST: eine Prioritätswarteschlange zum Ausführen einfacher Aufgaben.

In diesem Modell wird ein Thread als Unterklasse von a implementiert Task, die für einen späteren Zeitpunkt geplant ist (und möglicherweise in regelmäßigen Abständen neu geplant wird, wenn es wie üblich LoopTaskstattdessen Unterklassen gibt ). Die run()Methode des Objekts wird aufgerufen, wenn die Aufgabe fällig wird. Die run()Methode erledigt einige fällige Arbeiten und gibt dann zurück (dies ist das kooperative Bit). In der Regel wird eine Art Zustandsmaschine verwaltet, um die Aktionen bei aufeinanderfolgenden Aufrufen zu verwalten (ein einfaches Beispiel ist die light_on_p_Variable im folgenden Beispiel). Es erfordert ein leichtes Überdenken der Code-Organisation, hat sich jedoch bei intensiver Nutzung als sehr flexibel und robust erwiesen.

Es ist agnostisch in Bezug auf die Zeiteinheiten, so dass es genauso glücklich ist, in Einheiten von millis()wie micros()oder jedem anderen Tick zu laufen, der praktisch ist.

Hier ist das 'Blink'-Programm, das mit dieser Bibliothek implementiert wurde. Dies zeigt nur eine einzige ausgeführte Aufgabe: In der Regel werden andere Aufgaben erstellt und innerhalb von gestartet 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());
}

Das sind "Run-to-Completion" -Aufgaben, oder?
Edgar Bonet

@EdgarBonet Ich bin mir nicht sicher, was du meinst. Nachdem die run()Methode aufgerufen wurde, wird sie nicht unterbrochen, sodass sie die Verantwortung hat, angemessen schnell fertig zu werden. In der Regel erledigt es jedoch seine Arbeit und plant sich dann LoopTaskfür einige Zeit selbst neu (möglicherweise automatisch, im Fall einer Unterklasse von ). Ein gängiges Muster besteht darin, dass die Aufgabe eine interne Zustandsmaschine verwaltet (ein einfaches Beispiel ist der light_on_p_obige Zustand), damit sie sich bei nächster Fälligkeit angemessen verhält.
Norman Gray

Ja, das sind Run-to-Completion-Tasks (RtC-Tasks): Es kann keine Task ausgeführt werden, bevor die aktuelle Task ihre Ausführung durch Zurückkehren von beendet hat run(). Dies steht im Gegensatz zu kooperativen Threads, die die CPU beispielsweise durch Aufrufen von yield()oder ausgeben können delay(). Oder präventive Threads, die jederzeit terminiert werden können. Ich halte die Unterscheidung für wichtig, da ich gesehen habe, dass viele Leute, die hier nach Threads suchen, dies tun, weil sie es vorziehen, blockierenden Code zu schreiben, anstatt Zustandsautomaten. Das Blockieren von echten Threads, die die CPU belasten, ist in Ordnung. Das Blockieren von RtC-Aufgaben ist nicht möglich.
Edgar Bonet

@EdgarBonet Es ist eine nützliche Unterscheidung, ja. Ich würde sowohl diesen Stil als auch den Yield-Stil als einfach verschiedene Stile von kooperativen Threads betrachten, im Gegensatz zu präemptiven Threads, aber es ist wahr, dass sie einen anderen Ansatz zur Codierung erfordern. Es wäre interessant, einen durchdachten und gründlichen Vergleich der verschiedenen hier genannten Ansätze zu sehen. Eine nette Bibliothek, die oben nicht erwähnt wurde, sind Protothreads . Ich finde Dinge zu kritisieren, aber auch zu loben. Ich bevorzuge (natürlich) meinen Ansatz, weil er am explizitesten erscheint und keine zusätzlichen Stapel benötigt.
Norman Gray

(Korrektur: Protothread wurde erwähnt, in Antwort des @ sachleen )
Norman Grau
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.