risorse | coroutine in c

Coroutine in C

Introduzione

Una coroutine è una speciale subroutine che può momentaneamente sospendere l'esecuzione e riprenderla in un secondo momento; laddove una subroutine, quando invocata, ritorna solo una volta completata, una coroutine può uscire anticipatamente e continuare l'escuzione alla chiamata successiva, a partire dal punto esatto in cui era stata interrotta.

Le coroutine consentono di realizzare una forma di concorrenza denominata collaborative multitasking, dove un processo – incarnato da una coroutine –, passa volontariamente il controllo al processo successivo prima di aver concluso il suo lavoro. Su architetture primitive, dove non è disponibile un sistema di multitasking non-preemptive, le coroutine possono rappresentare una valida alternativa al single threading.

Esempio

Supponiamo di disporre della seguente funzione, composta di tre sezioni distinte:

void f() {
  printf("avvio di f...\n");
  ...
  printf("fase 1 ok...\n");
  ...
  printf("fase 2 ok...\n");
  ...
  printf("fase 3 ok, finito!\n");
}

Quando la funzione viene invocata, questa viene eseguita sequenzialmente, dall'inizio alla fine:

#include <stdio.h>

void f() {
  ...
}

int main() {
  f();
}

/* output:
 *
 * avvio di f...
 * fase 1 ok...
 * fase 2 ok...
 * fase 3 ok, finito!
 */

Ciò che si vorrebbe è interrompere l'esecuzione al termine di ogni fase e ripassare il controllo al chiamante; questi, quando lo riterrà opportuno, invocherà nuovamente la funzione, che proseguirà dal punto in cui era stata interrotta:

void int f() {
  printf("avvio di f...\n");
  yield;
  ...
  printf("fase 1 ok...\n");
  yield;
  ...
  printf("fase 2 ok...\n");
  yield;
  ...
  printf("fase 3 ok, finito!\n");
  return 0;
}

int main() {
  while(f()) {
    printf("*** main ***\n");
  }
}

Il costrutto yield sospende momentaneamente l'esecuzione della funzione, segnalando al chiamante che la stessa non è ancora stata completata; in secondo luogo fa sì che, alla prossima chiamata, l'esecuzione della funzione non riprenda dall'inizio, ma dall'istruzione che lo segue.

La sospensione dell'esecuzione si può ottenere con un'istruzione return 1 che, oltre a causare l'uscita anticipata, segnala che la funzione non è stata completata, secondo la convenzione per cui la funzione ritorna un valore non nullo fin tanto che non è terminata; il ripristino del corretto punto di esecuzione si ottiene con uno switch che seleziona il frammento di codice da eseguire basandosi su una variabile statica che ne contiene l'indice:

int f() {
  static int cr_entry_point_ = 0;
  switch (cr_entry_point_) {
    case 0:
      printf("avvio di f...\n");
      return ++cr_entry_point_;
    case 1:
      printf("fase 1 ok...\n");
      return ++cr_entry_point_;
    case 2:
      printf("fase 2 ok...\n");
      return ++cr_entry_point_;
    case 3:
      printf("fase 3 ok, finito!\n");
  }
  return 0;
}

/* output:
 *
 * avvio di f...
 * *** main ***
 * fase 1 ok...
 * *** main ***
 * fase 2 ok...
 * *** main ***
 * fase 3 ok, finito!
 */

Il risultato ottenuto evidenzia l'esecuzione parziale di f().

CR_YIELD & Co.

Per ridurre il “rumore” causato dalle nuove istruzioni, che nulla hanno a che vedere col vero scopo di f(), si procede per passi; dapprima si introduce un tipo esplicito a rappresentare lo stato della coroutine:

typedef enum {
  CR_TERMINATED = 0,
  CR_SUSPENDED = 1,
} cr_state;

int cr_state f() {
  static int cr_entry_point_ = 0;
  switch (cr_entry_point_) {
    case 0:
      printf("avvio di f...\n");
      return ++cr_entry_point_;
      cr_entry_point_ = 1;
      return CR_SUSPENDED;
    case 1:
      printf("fase 1 ok...\n");
      return ++cr_entry_point_;
      cr_entry_point_ = 2;
      return CR_SUSPENDED;
    case 2:
      printf("fase 2 ok...\n");
      return ++cr_entry_point_;
      cr_entry_point_ = 3;
      return CR_SUSPENDED;
    case 3:
      printf("fase 3 ok, finito!\n");
  }
  return 0 CR_TERMINATED;
}

Si può notare, nei punti di sospensione, una sequenza di istruzioni ripetute:

cr_state f() {
  static int cr_entry_point_ = 0;
  switch (cr_entry_point_) {
    case 0:
      printf("avvio di f...\n");
      cr_entry_point_ = 1; return CR_SUSPENDED; case 1:    // <---
      printf("fase 1 ok...\n");
      cr_entry_point_ = 2; return CR_SUSPENDED; case 2:    // <---
      printf("fase 2 ok...\n");
      cr_entry_point_ = 3; return CR_SUSPENDED; case 3:    // <---
      printf("fase 3 ok, finito!\n");
  }
  return CR_TERMINATED;
}

Il frammento evidenziato è un ottimo candidato per una macro, non fosse per l'indice numerico che assume valori via via crescenti; d'altra parte, nulla ci obbliga ad usare un contatore, dato che è sufficiente che ogni clausola case ne usi uno univoco. Per questo scopo si può ad esempio usare il numero di linea del sorgente:

  switch (cr_entry_point_) {
    case 0:
      printf("avvio di f...\n");
      cr_entry_point_ = 1 __LINE__; return CR_SUSPENDED; case 1 __LINE__:
      printf("fase 1 ok...\n");
      cr_entry_point_ = 2 __LINE__; return CR_SUSPENDED; case 2 __LINE__:
      printf("fase 2 ok...\n");
      cr_entry_point_ = 3 __LINE__; return CR_SUSPENDED; case 3 __LINE__:
      printf("fase 3 ok, finito!\n");
  }

Ora le tre linee di codice sono identiche e possono essere sostituite da una macro:

#define CR_YIELD \
  cr_entry_point_ = __LINE__; return CR_SUSPENDED; case __LINE__:

cr_state f() {
  static int cr_entry_point_ = 0;
  switch (cr_entry_point_) {
    case 0:
      printf("avvio di f...\n");
      cr_entry_point_ = __LINE__; return CR_SUSPENDED; case __LINE__: CR_YIELD
      printf("fase 1 ok...\n");
      cr_entry_point_ = __LINE__; return CR_SUSPENDED; case __LINE__: CR_YIELD
      printf("fase 2 ok...\n");
      cr_entry_point_ = __LINE__; return CR_SUSPENDED; case __LINE__: CR_YIELD
      printf("fase 3 ok, finito!\n");
  }
  return CR_TERMINATED;
}

È possibile rendere obbligatorio il punto e virgola dopo l'istruzione CR_YIELD con la solita tecnica del do/while, perché una clausola case è ammessa anche all'interno di un compound statement (cfr. Duff’s Device):

#define CR_YIELD \
  do { \
    cr_entry_point_ = __LINE__; return CR_SUSPENDED; case __LINE__:; \
  } while (0)

cr_state f() {
  static int cr_entry_point_ = 0;
  switch (cr_entry_point_) {
    case 0:
      printf("avvio di f...\n");
      CR_YIELD;
      printf("fase 1 ok...\n");
      CR_YIELD;
      printf("fase 2 ok...\n");
      CR_YIELD;
      printf("fase 3 ok, finito!\n");
  }
  return CR_TERMINATED;
}

Non rimane che mascherare preambolo e postambolo della funzione con altre due macro:

#define CR_BEGIN(cr_name_) \
  cr_state cr_name_() { \
    static int cr_entry_point_ = 0; \
    switch (cr_entry_point_) { \
      case 0:

#define CR_END \
    } \
    return CR_TERMINATED; \
  }

La definizione della coroutine f diventa:

CR_BEGIN(f)
  printf("avvio di f...\n");
  CR_YIELD;
  printf("fase 1 ok...\n");
  CR_YIELD;
  printf("fase 2 ok...\n");
  CR_YIELD;
  printf("fase 3 ok, finito!\n");
CR_END

Resta un ultimo problema da affrontare: se la coroutine viene chiamata dopo che è terminata, viene eseguito nuovamente il codice che segue l'ultima istruzione CR_YIELD:

int main() {
  for (int i = 0; i < 7; ++i) {
    f();
    printf("*** main ***\n");
  }
}

/* output:
 *
 * avvio di f...
 * *** main ***
 * fase 1 ok...
 * *** main ***
 * fase 2 ok...
 * *** main ***
 * fase 3 ok, finito!
 * *** main ***
 * fase 3 ok, finito!    <--- !!!
 * *** main ***
 * fase 3 ok, finito!    <--- !!!
 * *** main ***
 * fase 3 ok, finito!    <--- !!!
 * *** main ***
 */

È allora necessario, prima di uscire definitivamente dalla coroutine, tenere traccia del fatto che la coroutine è stata completata, e tenerne debito conto nelle chiamate successive:

#define CR_BEGIN(cr_name_) \
  cr_state cr_name_() { \
    static int cr_entry_point_ = 0; \
    if (cr_entry_point_ == -1) \
      return CR_TERMINATED; \
    switch (cr_entry_point_) { \
      case 0:

#define CR_END \
      cr_entry_point_ = -1; \
    } \
    return CR_TERMINATED; \
  }

/* output:
 *
 * avvio di f...
 * *** main ***
 * fase 1 ok...
 * *** main ***
 * fase 2 ok...
 * *** main ***
 * fase 3 ok, finito!
 * *** main ***
 * *** main ***
 * *** main ***
 */

Poiché la variabile statica cr_entry_point_ assume solo valori non negativi durante l'esecuzione della coroutine – 0 all'inizio, __LINE__ crescenti nel prosieguo –, -1 è un valore sicuro da usare come valore sentinella per segnalare che la funzione è già stata completata.

Avvicendamento di coroutine

È relativamente semplice intercalare due o più coroutine: una volta dichiarate, è sufficiente invocarle nel while del main, che agisce come una sorta di scheduler (piuttosto sciocco, in realtà):

CR_BEGIN(g)
  printf("avvio di g...\n");
  printf("5...\n");
  CR_YIELD;
  printf("4...\n");
  printf("3...\n");
  printf("2...\n");
  CR_YIELD;
  CR_YIELD;
  printf("1...\n");
  CR_YIELD;
  printf("0!\n");
CR_END

int main() {
  while (f() | g())
    ;
}

/* output:
 *
 * avvio di f...
 * avvio di g...
 * 5...
 * fase 1 ok...
 * 4...
 * 3...
 * 2...
 * fase 2 ok...
 * fase 3 ok, finito!
 * 1...
 * 0!
 */

Si noti l'uso dell'operatore bit-wise or `|`, che a differenza di quello logico non è corto-circuitato, e di come in corrispondenza di ogni CR_YIELD avviene uno switch di contesto.

CR_YIELD all'interno di compound statements

È possibile inserire un'istruzione CR_YIELD anche all'interno di cicli for/while, avendo l'accortezza di dichiarare static tutte le variabili in gioco, per essere sicuri di ritrovarle correttamente istanziate al successivo rientro:

CR_BEGIN(g)
  printf("avvio di g...\n");
  static int i = 0;
  for (i = 0; i < 5; ++i) {
    printf("%d...\n", 5 - i);
    CR_YIELD;
  }
  printf("0!\n");
CR_END

/* output:
 *
 * avvio di f...
 * avvio di g...
 * 5...
 * fase 1 ok...
 * 4...
 * fase 2 ok...
 * 3...
 * fase 3 ok, finito!
 * 2...
 * 1...
 * 0!
 */

Se correttamente configurati, gran parte dei compilatori emettono un warning qualora ci si dimentichi di dichiarare satic la variabile di controllo del ciclo:

CR_BEGIN(g)
  printf("avvio di g...\n");
  static int i = 0;
  for (i = 0; i < 5; ++i) {
    printf("%d...\n", 5 - i);
    CR_YIELD;
  }
  printf("0!\n");
CR_END



gcc 5.2.1 (-Wall)
--------------------------------------------------------------------------------
test.c:53:24: warning: ‘i’ may be used uninitialized in this function [-Wmaybe-u
ninitialized]
     for (i = 0; i < 5; ++i) {
                        ^

clang 3.6.0 (-Wall)
--------------------------------------------------------------------------------
test.c:53:26: warning: variable 'i' is uninitialized when used here
      [-Wuninitialized]
    for (i = 0; i < 5; ++i) {
                         ^
test.c:52:5: note: variable 'i' is declared here
    int i = 0;
    ^

Visual C++ 19.0 (/W4)
--------------------------------------------------------------------------------
test.c(55): error C2360: initialization of 'i' is skipped by 'case' label
test.c(52): note: see declaration of 'i'

CR_RUN

Le macro CR_RUN_ANY e CR_RUN_ALL definiscono due semplici scheduler di coroutine che, rispettivamente, terminano il programma quando termina la prima/tutte le coroutine elencate:

#define CR_RUN_ALL(...) \
    typedef cr_state (*cr_coroutine_)(); \
    cr_coroutine_ cr_pool_[] = { __VA_ARGS__, NULL }; \
    int main() { \
        int cr_all_terminated_ = 0; \
        cr_coroutine_* cr_next_ = NULL; \
        do { \
            cr_all_terminated_ = 1; \
            cr_next_ = cr_pool_; \
            while (*cr_next_ != NULL) { \
                if ((*cr_next_)() == CR_SUSPENDED) \
                    cr_all_terminated_ = 0; \
                ++cr_next_; \
            } \
        } while (!cr_all_terminated_); \
    }

#define CR_RUN_ANY(...) \
    typedef cr_state (*cr_coroutine_)(); \
    cr_coroutine_ cr_pool_[] = { __VA_ARGS__, NULL }; \
    int main() { \
        cr_coroutine_* cr_next_ = NULL; \
        for (;;) { \
            cr_next_ = cr_pool_; \
            while (*cr_next_ != NULL) { \
                if ((*cr_next_)() == CR_TERMINATED) \
                    return 0; \
                ++cr_next_; \
            } \
        } \
    }

int main() {
  while (f() | g())
    ;
}

CR_RUN_ALL(f, g)

/* output:
 *
 * avvio di f...
 * avvio di g...
 * 5...
 * fase 1 ok...
 * 4...
 * fase 2 ok...
 * 3...
 * fase 3 ok, finito!
 * 2...
 * 1...
 * 0!
 */

L'uso delle due macro è alternativo alla definizione della funzione main.

Poiché i compilatori più diffusi verificano che le funzioni dichiarate static vengano effettivamente invocate nell'unità di traduzione in cui sono state definite, si può sfruttare questa caratteristica per controllare che il pool di coroutine comprenda le tutte:

#define CR_BEGIN(cr_name_) \
  static cr_state cr_name_() { \
    static int cr_entry_point_ = 0; \
    if (cr_entry_point_ == -1) \
      return CR_TERMINATED; \
    switch (cr_entry_point_) { \
      case 0:

CR_RUN_ALL(f, g)



gcc 5.2.1 (-Wall)
--------------------------------------------------------------------------------
test.c:86:10: warning: ‘g’ defined but not used [-Wunused-function]
 CR_BEGIN(g)
          ^
test.c:31:21: note: in definition of macro ‘CR_BEGIN’
     static cr_state cr_name_() { \

clang 3.6.0 (-Wall)
--------------------------------------------------------------------------------
test.c:86:10: warning: unused function 'g' [-Wunused-function]
CR_BEGIN(g)
         ^
test.c:31:21: note: expanded from macro 'CR_BEGIN'
    static cr_state cr_name_() { \

CR_EXIT

Può a volte essere necessario terminare anzitempo una coroutine; la macro CR_EXIT serve allo scopo:

#define CR_EXIT \
  do { \
    cr_entry_point_ = -1; \
    return CR_TERMINATED; \
  } while (0)

CR_BEGIN(h)
  printf("avvio di h...\n");
  CR_EXIT;
  printf("qui non ci si arrivera' mai!\n");
CR_END

CR_RUN_ALL(f, g, h)

/* output:
 *
 * avvio di f...
 * avvio di g...
 * 5...
 * avvio di h...
 * fase 1 ok...
 * 4...
 * fase 2 ok...
 * 3...
 * fase 3 ok, finito!
 * 2...
 * 1...
 * 0!
 */

L'istruzione printf che segue la CR_EXIT nella coroutine h non è stata eseguita.

CR_YIELD_UNTIL/CR_YIELD_WHILE

Spesso accade che l'avanzamento di una coroutine dipenda da una condizione booleana; a tal scopo si definiscono due specializzazioni di CR_YIELD, una che attende che un'espressione booleana diventi true, l'altra false:

#define CR_YIELD_WHILE(cr_flag_) \
  do { \
    cr_entry_point_ = __LINE__; \
    case __LINE__:; \
    if (cr_flag_) \
      return CR_SUSPENDED; \
  } while (0)

#define CR_YIELD_UNTIL(cr_flag_) \
  CR_YIELD_WHILE(!(cr_flag_))

A differenza dell'istruzione CR_YIELD incondizionata, in CR_YIELD_WHILE il punto di rientro precede l'istruzione di return, dato che la condizione dev'essere rivalutata ad ogni successiva riattivazione della coroutine. La macro CR_YIELD_UNTIL è definita come duale di CR_YIELD_WHILE. Segue un esempio d'uso di CR_YIELD_WHILE:

int counter = 0;

CR_BEGIN(increment)
  while (counter++ < 10) {
    printf("%d...\n", counter);
    CR_YIELD;
  }
CR_END

CR_BEGIN(trigger)
  printf("trigger pronto...\n");
  CR_YIELD_WHILE(counter < 7);
  /* CR_YIELD_UNTIL(counter == 7); */
  printf("trigger scattato!\n");
CR_END

CR_RUN_ANY(increment, trigger)

/* output:
 *
 * 1...
 * trigger pronto...
 * 2...
 * 3...
 * 4...
 * 5...
 * 6...
 * 7...
 * trigger scattato!
 */

Coroutine rientranti

Una coroutine si dice rientrante se il suo codice può essere eseguito “simultaneamente” da thread diversi, senza che questi si influenzino a vicenda:

CR_RUN_ALL(f, f)

/* output:
 *
 * avvio di f...
 * fase 1 ok...
 * fase 2 ok...
 * fase 3 ok, finito!
 */

f è decisamente non-rientrante: anziché mostrare due esecuzioni concorrenti dello stesso codice, il risultato ottenuto è indistinguibile da quello prodotto in modalità single-thread. Ciò dipende dal fatto che le due incarnazioni di f condividono il valore della variabile statica cr_entry_point_, per cui ad ogni switch di contesto un thread prosegue dal punto in cui si è interrotto il precedente. Per rendere indipendenti le due esecuzioni, è necessario che ognuna di esse possieda un'istanza privata della variabile cr_entry_point_:

#define CR_BEGIN_REENTRANT(cr_name_) \
  static cr_state cr_name_(int* cr_context) { \
    if (cr_entry_point_ == -1) \
      return CR_TERMINATED; \
    switch (cr_entry_point_) { \
      case 0:

#define cr_entry_point_ (*cr_context)

CR_BEGIN_REENTRANT(f)
  printf("avvio di f...\n");
  CR_YIELD;
  printf("fase 1 ok...\n");
  CR_YIELD;
  printf("fase 2 ok...\n");
  CR_YIELD;
  printf("fase 3 ok, finito!\n");
CR_END

int main() {
  int f1 = 0;
  int f2 = 0;
  while (f(&f1) | f(&f2))
    ;
}

/* output:
 *
 * avvio di f...
 * avvio di f...
 * fase 1 ok...
 * fase 1 ok...
 * fase 2 ok...
 * fase 2 ok...
 * fase 3 ok, finito!
 * fase 3 ok, finito!
 */

I due thread procedono ora paralleli, senza interferenze. Da notare che per consentire il riutilizzo delle macro precedentemente definite nel nuovo contesto rientrante si è fatto ricorso ad una brutale sostituzione della variabile cr_entry_point_ con l'espressione (*cr_context); tale sostituzione non rende più possibile la definizione di coroutine convenzionali:

CR_BEGIN_REENTRANT(f)
  ...
CR_END

CR_BEGIN(g)
  printf("avvio di g...\n");
  static int i = 0;
  for (i = 0; i < 5; ++i) {
    printf("%d...\n", 5 - i);
    CR_YIELD;
  }
  printf("0!\n");
CR_END

int main() {
  int f1 = 0;
  int f2 = 0;
  while (f(&f1) | f(&f2) | g())
    ;
}

/* output:
 *
 * Segmentation fault (core dumped)
 */

Per ovviare a questo problema si può introdurre la convenzione di definire dapprima le coroutine rientranti, quindi quelle convenzionali, avendo cura di disattivare tra le une e le altre la riscrittura della variabile cr_entry_point_:

CR_BEGIN_REENTRANT(f)
  ...
CR_END

/*
 * fin qui solo coroutine rientranti
 */

#undef cr_entry_point_

/*
 * da qui in poi solo coroutine non-rientranti
 */

CR_BEGIN(g)
  ...
CR_END

int main() {
  int f1 = 0;
  int f2 = 0;
  while (f(&f1) | f(&f2) | g())
    ;
}

/* output:
 *
 * avvio di f...
 * avvio di f...
 * avvio di g...
 * 5...
 * fase 1 ok...
 * fase 1 ok...
 * 4...
 * fase 2 ok...
 * fase 2 ok...
 * 3...
 * fase 3 ok, finito!
 * fase 3 ok, finito!
 * 2...
 * 1...
 * 0!
 */

Per rendere la direttiva #undef più parlante, conviene rinominare la variabile cr_entry_point_ in cr_reentrancy_. In alternativa, è possibile separare le definizioni per le coroutine non rientranti e quelle rientranti in due header file separati, e definire le coroutine convenzionali prima di includere il secondo header:

#include "coroutines.h"

/*
 * definizione delle coroutine non rientranti
 */

CR_BEGIN(g)
  ...
CR_END

...

#include "reentrant-coroutines.h"

/*
 * definizione delle coroutine rientranti
 */

CR_BEGIN_REENTRANT(f)
  ...
CR_END

...

Contesti

Anziché utilizzare un intero per conservare il punto di rientro della coroutine, si può utilizzare una struttura dedicata, cui l'utente può eventualmente aggiungere delle variabili di comodo, se lo ritiene opportuno:

#define CR_BEGIN_REENTRANT(cr_name_) \
    static cr_state cr_name_(int cr_ ## cr_name_ ## _context * cr_context) { \
        if (cr_reentrancy_ == -1) \
            return CR_TERMINATED; \
        switch (cr_reentrancy_) { \
            case 0:

#define cr_reentrancy_ (*cr_context->cr_entry_point_)

typedef struct cr_f_context {
  int cr_entry_point_;
} cr_f_context;

CR_BEGIN_REENTRANT(f)
  ...
CR_END

#undef cr_reentrancy_

typedef struct cr_g_context {
  int i;
  int cr_entry_point_;
} cr_g_context;

CR_BEGIN_REENTRANT(g)
  printf("avvio di g...\n");
  static int i = 0;
  for (cr_context->i = 0; cr_context->i < 5; ++cr_context->i) {
  printf("%d...\n", 5 - cr_context->i);
  CR_YIELD;
  }
  printf("0!\n");
CR_END

int main() {
  int f1 = 0;
  int f2 = 0;
  cr_f_context f1 = { 0 };
  cr_f_context f2 = { 0 };
  cr_g_context g1 = { 0, 0 };
  while (f(&f1) | f(&f2) | g(&g1))
    ;
}

La definizione delle strutture di contesto può essere resa più compatta grazie ad alcune macro:

#define CR_BEGIN_REENTRANT(cr_name_) \
  typedef struct cr_ ## cr_name_ ## _context cr_ ## cr_name_ ## _context; \
  static cr_state cr_name_(cr_ ## cr_name_ ## _context* cr_context) { \
    if (cr_reentrancy_ == -1) \
      return CR_TERMINATED; \
    switch (cr_reentrancy_) { \
      case 0:

#define CR_CONTEXT_FOR(cr_name_) \
  struct cr_ ## cr_name_ ## _context { \

#define CR_END_CONTEXT \
    int cr_entry_point_; \
  };

#define CR_EMPTY_CONTEXT_FOR(cr_name_) \
  CR_CONTEXT_FOR(cr_name_) \
  CR_END_CONTEXT



typedef struct cr_f_context {
  int cr_entry_point_;
} cr_f_context;

CR_EMPTY_CONTEXT_FOR(f)

CR_BEGIN_REENTRANT(f)
  ...
CR_END

typedef struct cr_g_context {
  int i;
  int cr_entry_point_;
} cr_g_context;

CR_CONTEXT_FOR(g)
  int i;
CR_END_CONTEXT

CR_BEGIN_REENTRANT(g)
  ...
CR_END

Da notare che non è possibile definire pool contenenti coroutine rientranti con le macro CR_RUN_ALL/CR_RUN_ANY: l'allocazione del corretto numero e tipo di contesti, così come il lancio del corretto numero di istanze di coroutine è a carico all'utente.

CR_YIELD_FOR

Avendo a disposizione una funzione che ritorna un'indicazione del passare del tempo (ad esempio il numero di millisecondi trascorsi da un certo istante del passato) è possibile definire una forma di CR_YIELD che sospende l'esecuzione della coroutine per un certo intervallo di tempo:

/* versione non rientrante */

#define CR_YIELD_FOR(cr_interval_) \
  do { \
    static unsigned long cr_timer_ ## __LINE__; \
    cr_timer_ ## __LINE__ = cr_elapsed_ms(); \
    cr_entry_point_ = __LINE__; \
    case __LINE__:; \
    if (cr_elapsed_ms() - cr_timer_ ## __LINE__ < cr_interval_) \
      return CR_SUSPENDED; \
  } while (0)
/* versione rientrante */

#define CR_END_CONTEXT \
    unsigned long timer_; \
    int entry_point_; \
  };

#undef CR_YIELD_FOR

#define CR_YIELD_FOR(cr_interval_) \
  do { \
    cr_context->timer_ = cr_elapsed_ms(); \
    cr_entry_point_ = __LINE__; \
    case __LINE__:; \
    if (cr_elapsed_ms() - cr_context->timer_ < cr_interval_) \
      return CR_SUSPENDED; \
  } while (0)

Un esempio di implementazione della funzione ausiliaria cr_elapsed_ms:

#include <sys/timeb.h> /* ftime */

unsigned long cr_elapsed_ms() {
  static struct timeb t;
  ftime(&t);
  return (unsigned long)(t.time) * 1000 + t.millitm;
}

Riduzione del busy-waiting in CR_RUN_ALL/CR_RUN_ANY

Nel caso la piattaforma in uso consenta la sospensione temporanea dei thread, è possibile ridurre l'impatto sul processore interrompendo momentaneamente lo scheduler, per esempio al termine del ciclo di attivazione delle coroutine registrate:

#define CR_RUN_ALL_WITH_PAUSE(cr_period_, ...) \
  typedef cr_state (*cr_coroutine_)(); \
  cr_coroutine_ cr_pool_[] = { __VA_ARGS__, NULL }; \
  int main() { \
    int cr_all_terminated_ = 0; \
    cr_coroutine_* cr_next_ = NULL; \
    unsigned long cr_start_ = 0; \
    unsigned long cr_elapsed_ = 0; \
    do { \
      cr_all_terminated_ = 1; \
      cr_next_ = cr_pool_; \
      cr_start_ = cr_elapsed_ms(); \
      while (*cr_next_ != NULL) { \
        if ((*cr_next_)() == CR_SUSPENDED) \
          cr_all_terminated_ = 0; \
        ++cr_next_; \
      } \
      cr_elapsed_ = cr_elapsed_ms() - cr_start_; \
      if (cr_elapsed_ < cr_period_) \
        cr_sleep_ms(cr_period_ - cr_elapsed_); \
    } while (!cr_all_terminated_); \
  }

/* similmente per CR_RUN_ANY */

La sospensione temporanea dello scheduler può portare a notevoli risparmi energetici su sistemi embedded ed aumentare la reattività di sistemi multi-threading con scarse risorse computazionali. Segue infine un esempio di implementazione per cr_sleep_ms:

#ifdef __linux__
#include <unistd.h> /* usleep */
#endif

#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h> /* Sleep */
#endif

void cr_sleep_ms(unsigned long duration) {
#ifdef __linux__
  usleep(duration * 1000);
#endif
#ifdef _WIN32
  Sleep(duration);
#endif
}

Sorgenti

Riferimenti

  1. Kerr, K. "Windows with C++ - Lightweight Cooperative Multitasking with C++". MSDN Magazine. <http://msdn.microsoft.com/en-us/magazine/jj553509.aspx>. Visitato il 6 novembre 2015.
  2. Tatham, S. "Coroutines in C". chiark.greenend.org.uk. <http://www.chiark.greenend.org.uk/~sgtatham/coroutines.html>. Visitato il 6 novembre 2015.
  3. Dunkels, A. "How protothreads really work". dunkels.com. <http://dunkels.com/adam/pt/expansion.html>. Visitato il 6 novembre 2015.

Pagina modificata il 12/11/2015