Dart: gestire processi paralleli con la libreria dart:isolates

Di - 12 January 2013 - in

Riprende, dopo la pausa natalizia, la rubrica Impara Dart e mettilo da part con un articolo sulla libreria dart:isolate che definisce funzioni dedicate alla gestione di processi paralleli. Prima di iniziare è d’obbligo annunciare che da oggi il codice delle applicazioni e degli esempi trattati negli articoli sarà disponibile sull’account di Engeene su GitHub.

Il codice all’interno di un isolate viene eseguito in un contesto separato dal resto del programma ed ha una propria gestione della memoria, garantendo dunque che gli altri isolates non possano accedere direttamente ai valori che esso contiene. La funzione main() è un isolate e rappresenta il root isolate essendo il primo ad essere avviato. Se non diversamente specificato, quando l’esecuzione di un isolate termina, terminano anche tutti gli isolates da esso avviati. Vediamo un primo esempio pratico:

import 'dart:isolate';

saluta() {
  new Timer(1000, (Timer t) => print('saluti!'));
}

main() {
  print('Inizio');
  spawnFunction(saluta);
  print('Fine');
}

Output:

Inizio
Fine

Nota: nelle funzioni si può usare la sintassi () => istruzione() quando il corpo della funzione è costituito da una sola istruzione.

Un isolate si avvia con la funzione spawnFunction(saluta); l’isolate saluta() avvia un timer e dopo 1 secondo stampa un testo. Nell’output del programma però non vedremo il testo stampato da saluta() in quanto l’esecuzione di main() termina prima della scadenza del timer. Proviamo ad impostare un timer in main() tale che saluti() abbia il tempo necessario per essere eseguito:

import 'dart:isolate';

saluta() {
  new Timer(1000, (Timer t) => print('saluti!'));
}

main() {
  print('Inizio');
  spawnFunction(saluta);
  print('Fine');
  new Timer(2000, (Timer t) {
    return;
  });
}

Output:

Inizio
Fine
saluti!

Gli isolates non possono condividere uno stesso processo, ma possono comunicare fra loro tramite uno scambio di messaggi che avviene fra un oggetto SendPort e un ReceivePort. Il contenuto di questi messaggi può essere un oggetto primitivo (String, int, bool, etc), una mappa, una lista o un’istanza di SendPort. È importante notare che se in un isolate è attiva un’istanza di ReceivePort rimasta “in attesa” di un messaggio, l’isolate non terminerà fino alla ricezione di quel messaggio. Riscriviamo l’esempio precedente utilizzando le nozioni di SendPort e ReceivePort:

main() {
  print('Inizio');
  SendPort sPort = spawnFunction(saluti);
  print('Fine');
  ReceivePort rPort = new ReceivePort();
  rPort.receive((msg,_){
    print('Messaggio ricevuto, main() può terminare');
    rPort.close();
  });
}

Sappiamo che main() non riceverà alcun messaggio, quindi l’applicazione resterà in esecuzione fino alla chiusura forzata.

Facciamo qualche altra modifica al codice. Assumiamo che main() invii una lista di stringhe a saluti() che a sua volta le stamperà e invierà un messaggio conclusivo a main(); assumiamo inoltre che il messaggio che main() aspetta da saluti() sia "ok":

import 'dart:isolate';

saluti() {
  port.receive((messaggio,porta){
    for(var i=0; i<messaggio.length; i++ ) {
      print(messaggio[i]);
    }
    porta.send("ok");
  });
}

main() {

  SendPort sPort = spawnFunction(saluti);
  ReceivePort rPort = new ReceivePort();
  rPort.receive((messaggio,porta){
    if(messaggio=="ok"){
      print('Messaggio ricevuto, main() può ora terminare');
      rPort.close();
    }
  });

  sPort.send(['Ciao','Hello','Hola'], rPort.toSendPort());
}

Notiamo che il metodo SendPort.send() accetta due parametri in ingresso, il primo identifica il messaggio da inviare, il secondo identifica la porta attraverso la quale il destinatario dovrà inviare un (eventuale) messaggio di risposta. L’output sarà dunque:

Ciao
Hello
Hola
Messaggio ricevuto, main() può ora terminare

Prima di concludere diamo un’occhiata al metodo asincrono SendPort.call():

 main() {

  SendPort sPort = spawnFunction(saluti);
  sPort.call(['Ciao','Hello','Hola']).then((messaggio){
    if(messaggio=="ok") {
      print('Messaggio ricevuto, main() può ora terminare');
    }
  });
}

Questo metodo semplifica notevolmente il processo di comunicazione fra due isolates perché non richiede che sia stata esplicitamente istanziata la classe ReceivePort. Il metodo call() restituisce un istanza di Future, un oggetto di cui parleremo nel prossimo articolo a proposito della libreria dart:io, che contiene funzioni per lo sviluppo di applicazioni da riga di comando che richiedono un’interazione con il filesystem.

Leave a Reply

Claudio d'Angelis Articolo scritto da

Programmatore e studente di Informatica, appassionato di musica, web e sistemi UNIX. Collabora con Googlab dall'Ottobre 2012.

Contatta l'autore

Previous post:

Next post: