- Elettronica Open Source - https://it.emcelettronica.com -

Una libreria per thread

Utilizzare un kernel  [1]consente di sfruttare e gestire in modo estremamente flessibile il nostro design. Ma quale kernel utilizzare?

Non sempre è consigliabile acquistare un RTOS commerciale o utilizzare un kernel open source. In effetti, una scelta di questo tipo può anche non essere conveniente perché magari ci offre delle caratteristiche tecniche troppo complesse poi da gestire. L’alternativa è quella di costruircelo o utilizzarne uno già disponibile, ma anche una libreria di Thread può fare a caso nostro. Una libreria di questo tipo offre un insieme di primitive definite a peso leggero. Esistono diverse proposte in questo senso: da DosThread a RT-Thread Kernel, ognuna con differenti specifiche. DosThread è stato concepito inizialmente come un’insieme di primitive utilizzate in ambiente DOS, ma poi qualcuno si è spinto anche verso soluzioni di tipo embedded.

DOSTHREAD

DosThread offre un framework di lavoro per scrivere applicazioni di tipo multitasking in ambiente Dos. In realtà quello che DosThread propone è una sorta di multiplo (pseudo-) ambiente parallelo di esecuzione di tipo thread. La quasi totalità delle persone sanno che il sistema operativo [2]Dos non è stato pensato per ospitare applicazioni di tipo multithreading. Un Thread è creato utilizzando una classe, ossia si deve derivare una classe da DosThread che deve poi contenere il codice che si desidera eseguire attraverso una funzione chiamata “main”. In sostanza, è necessario dichiarare un’istanza della classe derivata e quindi chiamare la funzione membro “run” per iniziare l’esecuzione del Thread. L’esecuzione di un Thread è basato su un tick di sistema su 55ms. Ogni Thread viene eseguito in slot da 55ms prima di essere eventualmente sospeso per consentire ad altri Thread di controllare la CPU. Questo timeslice può essere modificato utilizzando la relativa funzione, “Timeslice”. Ma non solo, il timeslice può anche essere disabilitato trasferendo a carico di ciascun Thread la responsabilità di cedere volontariamente il controllo della CPU. DosThread offre una serie di servizi che il programmatore può utilizzare e a questo proposito la tabella 1 ne pone in evidenza alcuni di questi.

Tabella 1: alcune primitive disponibili.

Tabella 1: alcune primitive disponibili.

I Thread possono utilizzare la funzione di sistema Delay per ritardare l’esecuzione intervenendo direttamente sul numero degli impulsi di clock. Non solo, il Thread può anche utilizzare la funzione Pause o Terminate per terminare il Thread corrente o un altro attraverso un indentificatore. DosThread permette anche di controllare lo stato corrente di un Thread per mezzo della funzione Status. Esistono poi due classi addizionali: DOSMonitor e DOSMonitorQueue. Queste offrono meccanismi di sincronizzazione tra Thread per facilitare le loro comunicazioni. DosMonitor è utilizzato per proteggere anche strutture dati. Per evitare di accedere ad un variabile condivisa, globale, da parte di due o più Thread è possibile utilizzare la funzione lock e unlock. In questo caso con queste due funzioni si contrassegna una zona sicura, con lock si accede alla struttura desiderata, mentre unlock è la sua operazione complementare.

GESTIONE DI UN THREAD

Ogni Thread è creato dalla classe base DosThread. Ogni classe derivata deve fornire una definizione di una funzione membro chiamata “main” che contiene il codice che il thread dovrà eseguire, in questo modo:

void MyThread::main ()
{
   // code to be executed
   // by your thread
}

Il costruttore per la classe derivata “MyThread” invocherà il costruttore per DOSThread. Il costruttore per DOSThread richiede un singolo parametro che specifica la dimensione dello stack da assegnare per il thread. Tuttavia, il valore predefinito è di 2048 byte e se questo può risultare sufficiente non è necessario chiamare esplicitamente il costruttore DOSThread. In seguito, si può quindi dichiarare istanze di questa classe nel vostro programma, come ad esempio:

MyThread thread1;
   // a thread called “thread1”
MyThread threads [5];
// five identical threads

Il Thread dichiarato non sarà eseguito fino a quando si chiama la funzione membro “run”, come segue:

thread1.run ();

Ogni thread dispone di un timeslice di 55ms, click tick. Se un thread è ancora in esecuzione quando il suo timeslice scade, allora questo è messo in fondo alla coda ready-to-run ed è posto in esecuzione il thread successivo presente nella coda. La funzione statica “Timeslice” può essere utilizzata per modificare la lunghezza della timeslice utilizzata. Timeslice richiede un parametro che specifica la lunghezza desiderata ,in timeslice, degli impulsi di clock, come ad esempio:

DOSThread::timeslice (18);
// timeslice once a second
// (18 x 55ms)

Se il parametro è zero il timeslicing è disabilitato. In questo caso, spetta ai singoli thread cedere il controllo tra loro da chiamare una funzione membro che causerà un altro thread di essere programmato.

SCRIVERE LA FUNZIONE MAIN

“MyThread:: main” (la funzione principale della classe derivata) viene eseguita in parallelo con il resto del programma una volta invocata la chiamata alla funzione “run”. Mentre “MyThread:: Main” può essere scritta esattamente allo stesso modo di qualsiasi altra funzione, è altrettanto importante ricordare che condivide il processore con altri thread. La funzione di membro “pause permette di liberare temporaneamente il processore ad un altro thread:

pause ();
// schedule another thread

L’uso della funzione pause(), che tra l’altro è possibile invocare in qualsiasi punto del nostro programma come “DOSThread:: pause” anche se si utilizza timeslicing, è una buona idea per permettere ad un altro Thread di prendere il controllo del processore evitando di aspettare la fine del suo timeslice. Non solo, è anche possibile mettere il Thread corrente in uno stato di wait ad un tempo prestabilito utilizzando la funzione “Delay” e specificando il numero di clock tick.

delay (18);
// delay for 1 second
// (18 x 55ms)

Quando “MyThread::main” conclude anche il Thread termina. È comunque possibile terminare in modo esplicito il Thread utilizaando la funzione “terminate”. Così, per terminare il Thread “thread1” si deve ricorrere a:

thread1.terminate ();

INIZIALIZZARE E TERMINARE UN THREAD

Quando un thread viene dichiarato dal programma principale o da un altro thread, il costruttore per la classe DOSThread è invocato per creare il thread, oltre ad ogni costruttore definito dalla classe derivata. Quando si raggiunge la fine di un thread, il distruttore del thread corrente deve essere invocato. Qualsiasi distruttore fornito nella classe derivata è chiamato per primo (mentre il thread potrebbe essere ancora in esecuzione), il distruttore DOSThread è quindi invitato ad aspettare il termine del thread. Questo significa che il distruttore non dovrebbe fare nulla, la funzione di membro “wait” è utilizzata per garantire che il thread concluda le sue operazioni. In altre parole, il vostro distruttore dovrebbe essere scritto così:

MyThread::~MyThread ()
{
wait ();    // wait for thread
            // to terminate
…  // do any class-specific
   // tidying up
}

STATO DEL THREAD

Il membro della funzione “Status” permette di determinare lo stato di un thread in ogni momento. In effetti, attraverso la chiamata

Status = threadl.status();

è possibile ricavare, in ogni situazione, lo stato di un thread che deve rispecchiare quanto prevede il tipo DOSThread::State. La tabella 2 pone in evidenza i diversi stati possibili di un thread all’interno di DosTHread.

Tabella 2: DosThread: gli stati possibili

Tabella 2: DosThread: gli stati possibili

I MONITORS E LE COMUNICAZIONI TRA THREAD

Una delle maggiori preoccupazioni di un sistema di tipo multithread è sicuramente la gestione delle comunicazioni tra i diversi thread. Grazie a questo meccanismo è possibile determinare il ruolo e la situazione di ogni Thread. Uno dei classici problemi è la possibilità di modificare le variabili condivise, o variabili globali; in effetti, non si può sapere quando un Thread sarà rischedulato e non è nemmeno possibile sapere qual è il momento più sicuro per poter modificare le variabili condivise al fine di evitare degli effetti non desiderati. Per evitare questi problemi è stata prevista, in DosThread, la classe DosMonitor che permette, in modo sicuro, di svolgere operazioni di comunicazione interthread. Per utilizzare correttamente questa prerogativa è necessario derivare una classe da DOSMonitor che racchiude tutti i dati che dovranno essere aggiornati da uno o più thread: ogni funzione dovrebbe iniziare chiamando la funzione lock e al termine chiudere le operazioni con unlock. In questo modo si garantisce che solo un thread alla volta sta eseguendo una funzione di accesso. La struttura generale di questo meccanismo è possibile schematizzarlo in questo modo:

void MyMonitor::access
( /* parameter list */ )
{
lock ();
…  // access shared data
   // as required
unlock ();
}

Le classi derivate da DOSMonitor possono contenere le istanze di classe DOSMonitorQueue. All’interno di una funzione è possibile chiamare la funzione membro “Suspend” con DOSMonitorQueue come parametro per sospendere il thread in esecuzione. Ciò consentirà ad altri thread di eseguire funzioni di accesso all’interno di quella del monitor. La funzione statica “resume” può far riprendere l’esecuzione di un thread precedentemente sospeso su una coda: la funzione “resume” dispone di un parametro utilizzata per identificare la coda. Attraverso “resume” si risvegliano i thread sospesi su quella coda. Si noti che la sospensione dovrebbe essere invocata all’interno di un ciclo dal momento che “resume” riprenderà tutti i thread nella sua specifica coda. Non si garantisce nulla a proposito della condizione sui il thread risulta essere in attesa.

while (counter != 0)
  suspend (some_queue);

Il listato 3 mostra un esempio con un buffer di 20 caratteri. Il programma deve gestire il trasferimento dei venti caratteri tra thread differenti.

#include <stdio.h>
#include <conio.h>
#include “threads.h”
class Example1 : public DOSThread
{
  public:
    Example1 (int n) { num = n; }
  protected:
    virtual void main ();
  private:
    int num;
};
void Example1::main ()
{
   char c [70];
   sprintf (c, “Thread %d\n”, num);
   while (!userbreak())
   { fputs (c, stdout);
     pause ();
   }
}
void main ()
{
    Example1 e1 (1), e2 (2), e3 (3);
    puts (“Press CONTROL-BREAK to terminate”);
    puts (“Press any key to start…”);
    getch ();
    if (!e1.run ())
        puts (“Couldn’t start thread 1”);
    if (!e2.run ())
        puts (“Couldn’t start thread 2”);
    if (!e3.run ())
        puts (“Couldn’t start thread 3”);
}
Listato 1 – DosTHread, esempio
DOSThread::DOSThread (unsigned stacksize)
  : stack (mainsetup ? 0 :
             new char [stacksize > MIN_STACK ? stacksize : MIN_STACK]),
  entry (new DOSThreadManager (this)),
  state (TERMINATED)
{
 //—- leave thread terminated if any allocation failures have occurred
 if (!mainsetup && stack == 0)
      return;                // stack not allocated
 if (entry == 0)
     return;                 // thread queue entry not allocated
 //—- register thread creation
    DOSThreadManager::create ();
 //—- initialise new thread (for all but main thread)
 if (!mainsetup)
 {
 //—- set up stack pointer
        entry->stkptr = (unsigned*)(stack + stacksize);

       //—- create initial stack
       asm { sti; }                           // ensure interrupts enabled!
       *—(DOSThread**)(entry->stkptr) = this; // parameter for “start”
       entry->stkptr -= 2;                    // dummy return address
       *—(entry->stkptr) = _FLAGS;            // flags
       *—(entry->stkptr) = FP_SEG (&DOSThreadManager::start);        // cs
       *—(entry->stkptr) = FP_OFF (&DOSThreadManager::start);        // ip
       entry->stkptr -= 5;                 // ax, bx, cx, dx, es
       *—(entry->stkptr) = _DS;            // ds
       entry->stkptr -= 2;                 // si, di
       *—(entry->stkptr) = _BP;            // bp
       //—- stack extended registers on a 386 or above
       if (i386)
            entry->stkptr -= 18;            // 8 x 32-bit regs, fs, gs
    }
    //—- allow thread to live (but don’t move it into any queue)
    state = CREATED;
}
Listato 2 – DOSThread
class Buffer : public DOSMonitor
{
    char data[20];                   // the buffer itself
    int count;                       // no. Of chars in buffer
    int in;                          // where roced next char
    int out;                         // where to get next char from
    DOSMonitorQueue full;
    DOSMonitorQueue empty;
public:
    Buffer ()                         { count = in = out = 0; }
    void get (char& c);              // get a char from the buffer
    void put (char& c);              // put a char in the buffer
};
void Buffer::get (char& c)
{
   //—- lock the monitor against re-entry
   lock ();
   //—- suspend until the buffer isn’t empty
   while (count == 0)
       suspend (empty);
   //—- get next character from the buffer
   c = data [out++];
   out %= 20;
   //—- resume any threads waiting until buffer isn’t full
   resume (full);
   //—- unlock the monitor to let other threads in
   unlock ();
}
Listato 3 – Esempio con buffer