Jak mogę utworzyć wiele działających wątków?


60

Czy istnieje sposób, w jaki mogę mieć wiele części programu działających razem bez robienia wielu rzeczy w tym samym bloku kodu?

Jeden wątek czeka na urządzenie zewnętrzne, jednocześnie mrugając diodą LED w innym wątku.


3
Prawdopodobnie powinieneś najpierw zadać sobie pytanie, czy naprawdę potrzebujesz wątków. Czasomierze mogą już odpowiadać Twoim potrzebom i są natywnie obsługiwane na Arduino.
jfpoilpret

1
Możesz także sprawdzić Uzebox. Jest to dwuprocesorowa konsola do gier wideo homebrew. Więc choć nie jest to dokładnie Arduino, cały system jest zbudowany na przerwaniach. Tak więc audio, wideo, sterowanie itp. Są sterowane przerwaniami, podczas gdy główny program nie musi się o to martwić. Może być dobrym odniesieniem.
cbmeeks

Odpowiedzi:


50

W Arduino nie ma obsługi wieloprocesowej ani wielowątkowej. Możesz jednak zrobić coś blisko wielu wątków za pomocą oprogramowania.

Chcesz spojrzeć na Protothreads :

Protothreads to niezwykle lekkie wątki bez stosu, przeznaczone do systemów o bardzo ograniczonej pamięci, takich jak małe systemy osadzone lub węzły sieci czujników bezprzewodowych. Protothreads zapewniają liniowe wykonywanie kodu dla systemów sterowanych zdarzeniami zaimplementowanych w C. Protothreads mogą być używane z podstawowym systemem operacyjnym lub bez niego w celu zapewnienia blokowania obsługi zdarzeń. Protothreads zapewniają sekwencyjny przepływ kontroli bez skomplikowanych maszyn stanów lub pełnego wielowątkowości.

Oczywiście istnieje tutaj przykład Arduino z przykładowym kodem . To pytanie SO może być również przydatne.

ArduinoThread też jest dobry.


Zauważ, że Arduino DUE ma wyjątek od tego, z wieloma pętlami sterowania: arduino.cc/en/Tutorial/MultipleBlinks
tuskiomi

18

Arduino oparte na AVR nie obsługuje wątków (sprzętowych), nie znam Arduino opartych na ARM. Jednym ze sposobów obejścia tego ograniczenia jest użycie przerwań, zwłaszcza przerwania czasowego. Możesz zaprogramować timer, aby przerywał główną procedurę co tyle mikrosekund, aby uruchomić określoną inną procedurę.

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


15

Możliwe jest wykonywanie wielowątkowości po stronie oprogramowania na Uno. Gwintowanie na poziomie sprzętu nie jest obsługiwane.

Aby osiągnąć wielowątkowość, konieczne będzie wdrożenie podstawowego harmonogramu i utrzymanie listy procesów lub zadań w celu śledzenia różnych zadań, które należy uruchomić.

Struktura bardzo prostego harmonogramu nieprzekazującego byłaby następująca:

//Pseudocode
void loop()
{

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

}

Tutaj tasklistmoże być tablica wskaźników funkcji.

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

Z każdą funkcją formularza:

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

Każda funkcja może wykonywać osobne zadanie, takie jak function1wykonywanie manipulacji diodami LED i function2wykonywanie obliczeń zmiennoprzecinkowych. Każde zadanie (funkcja) będzie odpowiedzialne za przestrzeganie przydzielonego mu czasu.

Mam nadzieję, że to powinno wystarczyć, aby zacząć.


2
Nie jestem pewien, czy rozmawiałbym o „wątkach”, gdy używam nieprzewidywalnego harmonogramu. Nawiasem mówiąc, taki harmonogram już istnieje jako biblioteka arduino
jfpoilpret

5
@jfpoilpret - Wielowątkowość kooperacyjna to prawdziwa rzecz.
Connor Wolf,

Tak, masz rację! Mój błąd; tak dawno temu nie spotkałem się z wielowątkowością kooperacyjną, że moim zdaniem wielowątkowość musiała być zapobiegawcza.
jfpoilpret

9

Zgodnie z opisem twoich wymagań:

  • jeden wątek czeka na urządzenie zewnętrzne
  • jeden wątek miga diodą LED

Wygląda na to, że możesz użyć jednego przerwania Arduino dla pierwszego „wątku” (w rzeczywistości wolałbym to nazwać „zadaniem”).

Przerwania Arduino mogą wywoływać jedną funkcję (Twój kod) na podstawie zdarzenia zewnętrznego (poziom napięcia lub zmiana poziomu na cyfrowym pinie wejściowym), które natychmiast uruchomią twoją funkcję.

Jednak jedną ważną rzeczą, o której należy pamiętać w przypadku przerwań, jest to, że wywoływana funkcja powinna być tak szybka, jak to możliwe (zazwyczaj nie powinno być żadnego delay()wywołania ani żadnego innego interfejsu API, który byłby zależny delay()).

Jeśli musisz aktywować długie zadanie po wyzwoleniu zdarzenia zewnętrznego, możesz potencjalnie użyć harmonogramu kooperacyjnego i dodać do niego nowe zadanie z funkcji przerwania.

Drugą ważną kwestią dotyczącą przerwań jest to, że ich liczba jest ograniczona (np. Tylko 2 w UNO). Jeśli więc zaczniesz mieć więcej zdarzeń zewnętrznych, będziesz musiał zaimplementować pewnego rodzaju multipleksowanie wszystkich wejść w jednym, a funkcja przerwania określi, który zmultipleksowany inut był rzeczywistym wyzwalaczem.


6

Prostym rozwiązaniem jest użycie harmonogramu . Istnieje kilka implementacji. W skrócie opisano tę, która jest dostępna dla płyt opartych na AVR i SAM. Zasadniczo pojedyncze połączenie rozpocznie zadanie; „szkic w szkicu”.

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

Scheduler.start () doda nowe zadanie, które uruchomi polecenie taskSetup, a następnie wielokrotnie wywoła taskLoop, tak jak działa szkic Arduino. Zadanie ma swój własny stos. Rozmiar stosu jest parametrem opcjonalnym. Domyślny rozmiar stosu to 128 bajtów.

Aby umożliwić przełączanie kontekstu, zadania muszą wywoływać fed () lub delay () . Istnieje również makro obsługi oczekiwania na warunek.

await(Serial.available());

Makro to cukier składniowy dla następujących elementów:

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

Oczekiwania można także użyć do synchronizacji zadań. Poniżej znajduje się przykładowy fragment:

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

Aby uzyskać więcej informacji, zobacz przykłady . Istnieją przykłady od wielokrotnego mrugnięcia diody LED do przycisku odbicia i prostej powłoki z odczytem nieblokującego wiersza poleceń. Szablony i przestrzenie nazw mogą służyć do uporządkowania i ograniczenia kodu źródłowego. Poniższy szkic pokazuje, jak używać funkcji szablonu do wielokrotnego mrugania. Wystarczy 64 bajty na stos.

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

Istnieje również punkt odniesienia, aby dać wyobrażenie o wydajności, tj. Czas na rozpoczęcie zadania, zmianę kontekstu itp.

Na koniec istnieje kilka klas wsparcia dla synchronizacji i komunikacji na poziomie zadań; Kolejka i semafor .


3

Z poprzedniej inkantacji tego forum następujące pytanie / odpowiedź zostało przeniesione do inżynierii elektrycznej. Posiada przykładowy kod arduino do mrugania diody LED za pomocą przerwania timera podczas używania głównej pętli do szeregowego we / wy.

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

Repost:

Przerwania są powszechnym sposobem wykonywania zadań, gdy dzieje się coś innego. W poniższym przykładzie dioda LED miga bez użycia delay(). Przy każdym Timer1uruchomieniu isrBlinker()wywoływana jest procedura obsługi przerwań (ISR) . Włącza / wyłącza diodę LED.

Aby pokazać, że mogą się zdarzyć inne rzeczy, loop()wielokrotnie zapisuje foo / bar do portu szeregowego niezależnie od migania diody 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);
}

To jest bardzo proste demo. ISR mogą być znacznie bardziej złożone i mogą być wyzwalane przez timery i zdarzenia zewnętrzne (piny). Wiele wspólnych bibliotek jest implementowanych przy użyciu ISR.



2

Możesz także wypróbować moją bibliotekę ThreadHandler

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

Używa harmonogramu przerywającego, aby umożliwić przełączanie kontekstu bez przekazywania wydajności () lub opóźnienia ().

Stworzyłem bibliotekę, ponieważ potrzebowałem trzech wątków i potrzebowałem dwóch z nich, aby działały dokładnie w tym samym czasie, bez względu na to, co robili inni. Pierwszy wątek obsługiwał komunikację szeregową. Drugim działał filtr Kalmana wykorzystujący mnożenie macierzy zmiennoprzecinkowej za pomocą biblioteki Eigen. Trzeci to szybka nić pętli sterującej, która musiała być w stanie przerwać obliczenia macierzy.

Jak to działa

Każdy cykliczny wątek ma priorytet i kropkę. Jeśli wątek o wyższym priorytecie niż bieżący wątek osiągnie swój następny czas wykonania, program planujący wstrzyma bieżący wątek i przełączy się na wyższy. Po zakończeniu wykonywania wątku o wysokim priorytecie program planujący powraca do poprzedniego wątku.

Zasady planowania

Schemat szeregowania biblioteki ThreadHandler jest następujący:

  1. Najpierw najwyższy priorytet.
  2. Jeśli priorytet jest taki sam, najpierw wykonywany jest wątek o najwcześniejszym terminie.
  3. Jeśli dwa wątki mają ten sam termin, pierwszy utworzony wątek zostanie wykonany jako pierwszy.
  4. Wątek może być przerywany tylko przez wątki o wyższym priorytecie.
  5. Gdy wątek jest wykonywany, zablokuje wykonywanie dla wszystkich wątków o niższym priorytecie, aż do powrotu funkcji uruchamiania.
  6. Funkcja pętli ma priorytet -128 w porównaniu do wątków ThreadHandler.

Jak używać

Wątki można tworzyć poprzez dziedziczenie c ++

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

    virtual ~MyThread(){}

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

MyThread* threadObj = new MyThread();

Lub poprzez createThread i funkcję lambda

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

Obiekty wątków automatycznie łączą się z uchwytem wątków podczas ich tworzenia.

Aby rozpocząć wykonywanie utworzonych obiektów wątków, wywołaj:

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

1

A oto kolejna biblioteka wielozadaniowości współpracującej z mikroprocesorem - PQRST: kolejka priorytetowa do wykonywania prostych zadań.

W tym modelu wątek jest zaimplementowany jako podklasa a Task, która jest planowana na pewien czas w przyszłości (i być może planowana w regularnych odstępach czasu, jeśli, jak to zwykle bywa, podklasy LoopTask). run()Metoda obiektu jest wywoływana, gdy zadanie staje się wymagalne. run()Metoda ma pewne należytą pracę, a następnie zwraca (jest to spółdzielnia bit); zazwyczaj będzie utrzymywał maszynę stanu do zarządzania swoimi działaniami przy kolejnych wywołaniach (trywialnym przykładem jest light_on_p_zmienna w przykładzie poniżej). Wymaga to drobnego przemyślenia sposobu organizacji kodu, ale okazało się bardzo elastyczne i niezawodne przy dość intensywnym użyciu.

Jest agnostyczny w stosunku do jednostek czasu, więc jest równie szczęśliwy, biegając w jednostkach millis()co micros(), lub w dowolnym innym dogodnym tiku.

Oto program „blink” zaimplementowany przy użyciu tej biblioteki. Pokazuje to tylko jedno uruchomione zadanie: inne zadania byłyby zazwyczaj tworzone i uruchamiane w ramach 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());
}

To są zadania „do ukończenia”, prawda?
Edgar Bonet

@EdgarBonet Nie jestem do końca pewien, co masz na myśli. Po run()wywołaniu metody nie jest ona przerywana, więc jest odpowiedzialna za jej szybkie zakończenie. Zazwyczaj jednak wykona swoją pracę, a następnie przełoży się (ewentualnie automatycznie, w przypadku podklasy LoopTask) na pewien czas w przyszłości. Powszechnym wzorem dla zadania jest utrzymanie pewnej wewnętrznej maszyny stanów (trywialnym przykładem jest light_on_p_powyższy stan), aby zachowywała się odpowiednio, gdy będzie następna.
Norman Gray

Tak, są to zadania typu Run-to-Complete (RtC): żadne zadanie nie może zostać uruchomione, dopóki bieżące nie zakończy wykonywania przez powrót z run(). Jest to w przeciwieństwie do wątków kooperacyjnych, które mogą uzyskać wydajność procesora, np. Przez wywołanie yield()lub delay(). Lub wątki zapobiegawcze, które można zaplanować w dowolnym momencie. Uważam, że to rozróżnienie jest ważne, ponieważ zauważyłem, że wiele osób, które przychodzą tutaj w poszukiwaniu wątków, robi to, ponieważ wolą pisać kod blokujący niż maszyny stanowe. Blokowanie prawdziwych wątków, które dają procesor, jest w porządku. Blokowanie zadań RtC nie jest.
Edgar Bonet

@EdgarBonet To przydatne rozróżnienie, tak. Uważałbym, że zarówno ten styl, jak i wątki podatkowe, są po prostu różnymi stylami wątków kooperacyjnych, w przeciwieństwie do wątków wyprzedzających, ale prawdą jest, że wymagają one innego podejścia do kodowania ich. Interesujące byłoby rozważne i dogłębne porównanie różnych wymienionych tu podejść; jedną fajną biblioteką nie wymienioną powyżej są protothreads . W obu znajduję rzeczy do krytykowania, ale także pochwały. Ja (oczywiście) wolę moje podejście, ponieważ wydaje się najbardziej jednoznaczne i nie wymaga dodatkowych stosów.
Norman Gray,

(korekta: protothreads zostało wspomniane, w odpowiedzi na @ sachleen )
Norman Grey
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.