Birden fazla çalışan iş parçacığı nasıl oluşturabilirim?


59

Aynı kod bloğunda birden fazla şey yapmadan programın birden fazla bölümünü birlikte çalıştırmamın bir yolu var mı?

Bir iş parçacığı, başka bir iş parçacığında bir LED'i yanıp sönerken harici bir aygıtı beklerken.


3
Muhtemelen önce iplere ihtiyacınız olup olmadığını kendinize sormalısınız. Zamanlayıcılar ihtiyaçlarınız için zaten uygun olabilir ve bunlar Arduino'da yerel olarak desteklenir.
jfpoilpret

1
Uzebox'u da kontrol etmek isteyebilirsiniz. İki yonga homebrew video oyun konsolu. Yani tam olarak bir Arduino olmasa da, tüm sistem kesintilere dayanıyor. Bu nedenle, ses, video, kontroller vb. Hepsi kesintiye uğrar, ancak ana programın herhangi biri için endişelenmesine gerek yoktur. İyi referans olabilir.
cbmeeks,

Yanıtlar:


50

Arduino'da çoklu süreç ya da çoklu iş parçacığı desteği yoktur. Yine de bazı yazılımlarla birden fazla iş parçacığına yakın bir şey yapabilirsiniz.

Protothreads'e bakmak istiyorsunuz :

Protothreads, küçük gömülü sistemler veya kablosuz sensör ağ düğümleri gibi ciddi oranda bellek kısıtlamalı sistemler için tasarlanmış son derece hafif yığınsız ipliklerdir. Protothreads, C'de uygulanan olaya dayalı sistemler için doğrusal kod yürütme sağlar. Protothreads, engelleyici olay işleyicileri sağlamak için temel bir işletim sistemiyle birlikte veya bunlar olmadan kullanılabilir. Protothreads karmaşık durum makineleri veya tam çoklu iş parçacığı olmadan art arda kontrol akışı sağlar.

Tabii ki, bir Arduino örnek var burada ile örnek kodu . Bu SO sorusu da faydalı olabilir.

ArduinoThread de iyi bir tanesidir.


Arduino DUE'nin
tuskiomi

18

AVR tabanlı Arduino (donanım) iş parçacığını desteklemiyor, ben ARM tabanlı Arduino'yu tanımıyorum. Bu sınırlamanın bir yolu, kesintilerin, özellikle zamanlanmış kesintilerin kullanılmasıdır. Belirli bir başka rutini çalıştırmak için her bir mikrosaniyede ana rutini kesmek için bir zamanlayıcı programlayabilirsiniz.

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


15

Uno'da yazılım tarafı çoklu iş parçacığı yapmak mümkündür. Donanım düzeyinde iş parçacığı desteklenmiyor.

Çok iş parçacığı elde etmek için, temel bir zamanlayıcının uygulanmasını ve çalıştırılması gereken farklı görevleri izlemek için bir işlem veya görev listesinin sürdürülmesini gerektirir.

Çok basit, preemptif olmayan bir programlayıcının yapısı şöyle olacaktır:

//Pseudocode
void loop()
{

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

}

Burada, tasklistbir dizi işlev işaretçisi olabilir.

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

Formun her işlevi ile:

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

Her fonksiyon, function1LED manipülasyonları function2yapmak ve şamandıra hesaplamaları yapmak gibi ayrı bir görevi gerçekleştirebilir . Ayrılan zamana bağlı kalmak her bir görevin (işlevin) sorumluluğu olacaktır.

Umarım, başlamanız için bu yeterli olacaktır.


2
Önleyici olmayan bir zamanlayıcı kullanırken "konular" hakkında konuşacağımdan emin değilim. Bu arada, böyle bir zamanlayıcı zaten bir arduino kütüphanesi olarak var: arduino.cc/en/Reference/Scheduler
jfpoilpret

5
@jfpoilpret - Kooperatif çoklu okuma gerçek bir şey.
Connor Wolf

Evet haklısın! Benim hatam; çok uzun zaman önce çok aklımda olmamıştı ki çok akıl yürütmenin önleyici olması gerektiğine değindi.
jfpoilpret

9

Gereksinimlerinizin tanımına göre:

  • harici bir cihaz için bekleyen bir iş parçacığı
  • bir LED yanıp sönen bir iplik

İlk "thread" için bir Arduino kesmesi kullanabileceğinizi görünüyor (aslında "görev" demeyi tercih ederim).

Arduino kesmeleri, harici bir olaya (bir dijital giriş pimindeki voltaj seviyesi veya seviye değişimi) dayalı olarak bir işlevi (kodunuz) çağırabilir; bu, işlevinizi derhal tetikler.

Bununla birlikte, kesinti ile ilgili akılda tutulması gereken önemli bir nokta, çağrılan işlevin olabildiğince hızlı olması gerektiğidir (tipik olarak, herhangi bir delay()çağrı ya da bağımlı olabilecek herhangi bir API olmamalıdır delay()).

Harici olay tetikleyicide etkinleştirmek için uzun bir göreviniz varsa, o zaman potansiyel olarak bir işbirlikçi zamanlayıcı kullanabilir ve kesme işlevinizden yeni bir görev ekleyebilirsiniz.

Kesintilerle ilgili ikinci önemli nokta, sayılarının sınırlı olmasıdır (örneğin UNO'da sadece 2). Bu nedenle, daha fazla harici olay yaşamaya başlarsanız, tüm girdileri bir çoğullama ile bir çeşit çoğaltmaya uygulamanız ve kesme işlevinizin gerçek tetikleyicinin hangi çoğaltılmış inut olduğunu belirlemesini sağlamanız gerekir.


6

Basit bir çözüm bir Zamanlayıcı kullanmaktır . Birkaç uygulama var. Bu kısaca AVR ve SAM tabanlı kartlar için mevcut olanı açıklar. Temel olarak, tek bir çağrı bir görevi başlatacak; msgstr "eskiz içinde eskiz".

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

Scheduler.start (), bir kez taskSetup komutunu çalıştıracak yeni bir görev ekler ve Arduino eskizinin çalıştığı gibi tekrar tekrar taskLoop'u çağırır. Görevin kendi yığını var. Yığın boyutu isteğe bağlı bir parametredir. Varsayılan yığın boyutu 128 bayttır.

Bağlam geçişi sağlamak için görev geçişini verim () veya gecikme () olarak yapmanız gerekir . Bir koşulu beklemek için bir destek makrosu da vardır.

await(Serial.available());

Makro, aşağıdakiler için sözdizimsel şekerdir:

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

Bekleme, görevleri senkronize etmek için de kullanılabilir. Aşağıda bir örnek pasaj var:

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

Daha fazla ayrıntı için örneklere bakınız . Birden fazla LED yanıp sönme modundan çıkma düğmesine ve bloklamayan komut satırı okunan basit bir kabuğa örnekler vardır. Şablonlar ve ad alanları, kaynak kodunu yapılandırmaya ve azaltmaya yardımcı olmak için kullanılabilir. Aşağıdaki çizim , çoklu göz kırpma için şablon işlevlerinin nasıl kullanılacağını gösterir. Yığın için 64 bayt yeterlidir.

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

Bir de bulunmaktadır kıyaslama performansının bir fikir vermek için, yani zaman görevi, bağlam anahtarı, vb başlatmak için

Son olarak, görev düzeyinde senkronizasyon ve iletişim için birkaç destek sınıfı vardır; Sıra ve Semafor .


3

Bu forumun daha önceki tanıtımlarından, aşağıdaki soru / cevap Elektrik Mühendisliğine taşındı. Seri IO yapmak için ana döngüyü kullanırken bir zamanlayıcı kesmesi kullanarak bir LED'i yanıp sönen örnek arduino koduna sahiptir.

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

repost:

Kesintiler, başka bir şey devam ederken işleri halletmenin yaygın bir yoludur. Aşağıdaki örnekte, LED kullanmadan yanıp sönüyor delay(). Ne zaman Timer1ateş ederse, kesme servis rutini (ISR) isrBlinker()denir. LED'i açar / kapatır.

Aynı anda başka şeylerin de olabileceğini göstermek için, loop()art arda yanıp sönen ışıktan bağımsız olarak seri bağlantı noktasına foo / bar yazar.

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

Bu çok basit bir demo. ISR'ler çok daha karmaşık olabilir ve zamanlayıcılar ve dış olaylar (pimler) tarafından tetiklenebilir. Ortak kütüphanelerin çoğu ISR'ler kullanılarak uygulanır.



2

ThreadHandler kütüphaneme de bir denemek

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

Aktarım () veya gecikme () üzerine geçiş yapmadan bağlam geçişine izin vermek için bir kesme zamanlayıcısı kullanır.

Kütüphaneyi yarattım çünkü üç konuya ihtiyacım vardı ve diğerlerinin ne yaptığı önemli değil. İlk iş parçacığı seri iletişim ele aldı. İkincisi, Eigen kütüphanesi ile kayan matris çarpımını kullanan bir Kalman filtresi kullanıyordu. Üçüncüsü, matris hesaplamalarını durdurabilen hızlı bir akım kontrol döngüsü ipliği idi.

Nasıl çalışır

Her döngüsel iş parçacığının bir önceliği ve bir dönemi vardır. Geçerli yürütme iş parçacığından daha yüksek önceliğe sahip bir iş parçacığı bir sonraki yürütme süresine ulaşırsa, zamanlayıcı geçerli iş parçacığını duraklatır ve daha yüksek önceliğe geçer. Yüksek öncelikli iş parçacığı yürütmesini tamamladığında, zamanlayıcı önceki iş parçasına geri döner.

Planlama kuralları

ThreadHandler kütüphanesinin zamanlama şeması aşağıdaki gibidir:

  1. İlk önce en yüksek öncelik.
  2. Öncelik aynıysa, ilk önce son tarih verilen iş parçacığı önce yürütülür.
  3. İki iş parçacığı aynı son tarih varsa, ilk oluşturulan iş parçacığı ilk yürütülür.
  4. Bir diş sadece yüksek önceliğe sahip dişliler tarafından kesilebilir.
  5. Bir iş parçacığı yürütüldüğünde, çalışma işlevi geri dönene kadar daha düşük önceliğe sahip tüm iş parçacıklarının yürütülmesini engeller.
  6. Döngü işlevi ThreadHandler dişlileri ile karşılaştırıldığında -128 önceliğine sahiptir.

Nasıl kullanılır

Konular c ++ mirası yoluyla oluşturulabilir

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

    virtual ~MyThread(){}

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

MyThread* threadObj = new MyThread();

Veya createThread ve bir lambda işlevi aracılığıyla

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

Thread nesneleri, yaratıldıklarında ThreadHandler'a otomatik olarak bağlanır.

Oluşturulan thread nesnelerinin yürütülmesini başlatmak için:

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

1

Ve işte bir başka mikroişlemci işbirlikçi çoklu görev kütüphanesi - PQRST: Basit Görevler Çalıştırmak için bir Öncelik Kuyruğu.

Bu modelde, bir iş parçacığı, Taskileride bir süre için programlanmış bir a'nın bir alt sınıfı olarak uygulanır (ve eğer yaygınsa, bunun LoopTaskyerine alt sınıflar varsa düzenli aralıklarla yeniden planlanır ). run()Görev nedeniyle olduğunda nesne yöntemi olarak adlandırılır. run()Yöntem, bazıları iş yapar ve daha sonra (bu ortak bit) döndürür; Genelde art arda yapılan çağrılardaki eylemlerini yönetmek için bir tür durum makinesini koruyacaktır (önemsiz bir light_on_p_örnek aşağıdaki örnekteki değişkendir). Kodunuzu nasıl düzenlediğiniz konusunda biraz düşünmenizi gerektirir, ancak oldukça yoğun kullanımda çok esnek ve sağlam olduğu kanıtlanmıştır.

Zaman birimleri hakkında agnostik, bu yüzden birimlerinde çalışan olarak mutlu millis()olarak micros()veya uygun herhangi bir başka kene.

İşte bu kütüphaneyi kullanarak uygulanan 'göz kırpma' programı. Bu, çalışan tek bir görevi gösterir: diğer görevler genellikle oluşturulur ve içinde başlatılır 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());
}

Bunlar “tamamlama koşusu” görevleri, değil mi?
Edgar Bonet

@EdgarBonet Ne demek istediğinden emin değilim. run()Yöntem çağrıldıktan sonra kesintiye uğramaz, bu yüzden makul bir şekilde derhal bitirmek sorumluluğu vardır. Bununla birlikte, tipik olarak, işini yapacak ve daha sonra LoopTaskgelecek bir süre için kendini (muhtemelen bir alt sınıf durumunda otomatik olarak) yeniden planlayacaktır . Ortak bir düzen, görevin bazı iç durum makinelerini sürdürmesidir (önemsiz bir örnek light_on_p_yukarıdaki durumdur), böylece bir sonraki durumdayken uygun davranır.
Norman Gray

Öyleyse evet, bunlar tamamlama işlemi (RtC) görevleridir: şu anki görevden geri dönerek işlemi tamamlamadan önce hiçbir görev çalıştırılamaz run(). Bu, CPU'yu örneğin arayarak yield()ya da arayarak verebilecek olan ortak iş parçacıklarının aksinedir delay(). Veya herhangi bir zamanda planlanabilecek önleyici iplikler. Buradaki farkın önemli olduğunu düşünüyorum, çünkü buralarda iş arayan birçok insanın bunu yapmakta olduğunu gördüm, çünkü devlet makineleri yerine blok kod yazmayı tercih ediyorlar. İşlemciyi sağlayan gerçek dişlileri engellemek iyidir. RtC görevlerini engelleme değildir.
Edgar Bonet

@EdgarBonet Bu yararlı bir ayrım, evet. Hem bu tarzı hem de verim tarzı dişlileri, önleyici dişlilerin aksine, sadece farklı kooperatif iplik stilleri olarak görürdüm, ama onları kodlamak için farklı bir yaklaşım gerektirdikleri doğru. Burada belirtilen çeşitli yaklaşımların düşünceli ve derinlemesine bir karşılaştırmasını görmek ilginç olurdu; Yukarıda bahsedilmeyen güzel bir kütüphane protothreads . Her ikisinde de eleştirecek şeyler değil, aynı zamanda övgüler de bulacağım. Ben (elbette) yaklaşımımı tercih ediyorum, çünkü bu en belirgin görünüyor ve fazladan yığın gerektirmiyor.
Norman Gray

(düzeltme: protothreads edildi içinde, sözü @ sachleen cevabı )
Norman Gri
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.