Voi siete qui: Inizio Programmare in Scala

Programmazione concorrente robusta e scalabile con gli attori

I problemi dello stato condiviso e sincronizzato

La programmazione concorrente non è facile. Tradizionalmente, rendere un programma in grado di eseguire più di una singola attività alla volta ha sempre voluto dire faticare dietro a semafori, condizioni di corsa, contese di lock, e a tutto il resto dello sgradevole bagaglio che il multithreading si porta dietro. I modelli di concorrenza a eventi alleviano alcuni di questi problemi, ma possono trasformare programmi di grandi dimensioni in una rete intricata di funzioni di callback. Nessuna meraviglia, quindi, che la programmazione concorrente sia un compito che la maggior parte dei programmatori teme o evita del tutto ripiegando su modelli in cui una molteplicità di processi indipendenti condivide i dati esternamente (per esempio, attraverso un database o una coda di messaggi).

Una buona parte delle difficoltà della programmazione concorrente è causata dallo stato: come fate a sapere quello che il vostro programma multithread sta facendo e quando lo fa? Qual è il valore contenuto in una particolare variabile quando avete in esecuzione due thread, o cinque, o dieci? Come potete garantire che le diverse parti del vostro programma non lottino tra loro nel tentativo di prendere l’iniziativa? Un paradigma di concorrenza basato sui thread porta con sé più domande che risposte.

Per fortuna, Scala offre un approccio ragionevole e flessibile alla programmazione concorrente, che esploreremo in questo capitolo.

Gli attori

Nonostante possiate aver sentito citare Scala e gli attori nella stessa frase, gli attori non sono appannaggio esclusivo di Scala. Originariamente conceptiti per essere usati nel campo dell’Intelligenza Artificiale, gli attori sono apparsi per la prima volta nel 1973 (si vedano [Hewitt1973] e [Agha1987]). Da allora, sono comparse diverse variazioni dell’idea di attore in un certo numero di linguaggi di programmazione, in particolare Erlang e Io. Come astrazione, gli attori sono generali a sufficienza da poter essere implementati sotto forma di libreria (come in Scala) o di entità fondamentali di un sistema computazionale.

Gli attori in astratto

Fondamentalmente, un attore è un oggetto che riceve messaggi e agisce sulla base di quei messaggi. L’ordine in cui i messaggi arrivano non è importante per un attore, sebbene in alcune implementazioni (come quella di Scala) i messaggi vengano accodati in ordine. Un attore potrebbe gestire un messaggio internamente, o potrebbe inviare un mesaggio a un altro attore, o potrebbe creare un altro attore per fargli intraprendere un’azione sulla base del messaggio. Gli attori sono un’astrazione di livello molto alto.

A differenza dei tradizionali sistemi a oggetti (che, potreste dire a voi stessi, posseggono molte delle stesse proprietà che abbiamo descritto), gli attori non impongono una sequenza o un ordine alle loro azioni. È questa rinuncia intrinseca alla sequenzialità, combinata con l’indipendenza da uno stato globale condiviso, che permette agli attori di svolgere il proprio lavoro in parallelo. Come vedremo più avanti, l’uso giudizioso dei dati immutabili è particolarmente consono al modello degli attori, e contribuisce ulteriormente a rendere la programmazione concorrente sicura e comprensibile.

La teoria è sufficiente. Ora vediamo gli attori in pratica.

Gli attori in Scala

Nella loro essenza, gli attori in Scala sono oggetti che estendono scala.actors.Actor.

// esempi/cap-9/simple-actor-script.scala

import scala.actors.Actor

class Redford extends Actor {
  def act() {
    println("Recitare consiste, in buona parte, nel prestare attenzione.")
  }
}

val robert = new Redford
robert.start

Come possiamo vedere, un attore definito in questo modo deve essere sia istanziato sia fatto partire, in maniera simile a come Java gestisce i propri thread. Un attore deve anche implementare il metodo astratto act, che restituisce Unit. Una volta che abbiamo fatto partire questo semplice attore, sulla console viene stampato il saggio consiglio seguente.

Recitare consiste, in buona parte, nel prestare attenzione.

Il package scala.actors contiene un metodo factory per creare attori che evitano buona parte dei preparativi dell’esempio precedente. Possiamo importare questo metodo e altri metodi di convenienza da scala.actors.Actor._. Ecco un attore creato con il metodo factory.

// esempi/cap-9/factory-actor-script.scala

import scala.actors.Actor
import scala.actors.Actor._

val paulNewman = actor {
  println("Per essere un attore devi rimanere bambino.")
}

Mentre una sottoclasse che estende Actor deve definire act in modo da essere concreta, un attore costruito con il metodo factory non soffre di questa limitazione. In questo esempio più breve, il corpo del metodo passato ad actor viene effettivamente promosso al ruolo del metodo act nel nostro primo esempio. Com’è prevedibile, anche questo attore stampa un messaggio quando viene eseguito. Tutto ciò è molto interessante, ma non abbiamo ancora visto il pezzo fondamentale del puzzle degli attori: l’invio di messaggi.

Inviare messaggi agli attori

Gli attori possono ricevere qualsiasi tipo di oggetto come messaggio, dalle stringhe di testo ai tipi numerici a qualsiasi classe abbiate creato nei vostri programmi. È per questa ragione che gli attori e il pattern matching vanno a braccetto. Un attore dovrebbe agire solo in risposta a messaggi di tipi noti; l’uso del pattern matching sulla classe e/o sui contenuti di un messaggio è una buona strategia difensiva di programmazione e aumenta la leggibilità del codice dell’attore.

// esempi/cap-9/pattern-match-actor-script.scala

import scala.actors.Actor
import scala.actors.Actor._

val fussyActor = actor {
  loop {
    receive {
      case s: String => println("Ecco una String: " + s)
      case i: Int => println("Ecco un Int: " + i.toString)
      case _ => println("Non ho idea di cosa mi sia arrivato.")
    }
  }
}

fussyActor ! "Ehilà"
fussyActor ! 23
fussyActor ! 3.33

Quando viene eseguito, questo esempio stampa il testo seguente.

Ecco una String: Ehilà
Ecco un Int: 23
Non ho idea di cosa mi sia arrivato.

Il corpo di fussyActor è composto da un metodo receive racchiuso in un ciclo loop. In sostanza, loop è una gradevole abbreviazione per while (true): esegue ripetutamente ciò che si trova all’interno del suo blocco. receive si blocca fino a quando non riceve un messaggio di un tipo che soddisferà uno dei casi del suo pattern matching interno.

Le ultime righe di questo esempio mostrano l’uso del metodo ! (punto esclamativo) per inviare messaggi al nostro attore. Se avete già visto gli attori in Erlang, questa sintassi vi sarà familiare. L’attore è sempre sul lato sinistro del punto esclamativo, e il messaggio che viene inviato a quell’attore è sempre sul lato destro. Se avete bisogno di una mnemotecnica per questo granello di zucchero sintattico, immaginate di essere un regista arrabbiato che urla direttive ai propri attori.

La mailbox

Ogni attore possiede una mailbox (letteralmente, casella di posta) in cui vengono accodati i messaggi inviati a quell’attore. Ecco un esempio in cui ispezioniamo la dimensione della mailbox di un attore.

// esempi/cap-9/actor-mailbox-script.scala

import scala.actors.Actor
import scala.actors.Actor._

val countActor = actor {
  loop {
    react {
      case "quanti?" => {
        println("Ho " + mailboxSize.toString + " messaggi nella mia mailbox.")
      }
    }
  }
}

countActor ! 1
countActor ! 2
countActor ! 3
countActor ! "quanti?"
countActor ! "quanti?"
countActor ! 4
countActor ! "how many?"

Questo esempio produce l’uscita seguente.

Ho 3 messaggi nella mia mailbox.
Ho 3 messaggi nella mia mailbox.
Ho 4 messaggi nella mia mailbox.

Notate che i messaggi nella prima e nella seconda riga sono identici. Dato che il nostro attore è stato creato solamente per elaborare messaggi composti dalla stringa "quanti?", quei messaggi non sono rimasti nella sua mailbox. Solo i messaggi di tipo sconosciuto — in questo caso, Int — rimangono nella mailbox senza essere elaborati.

Se vedete che la dimensione della mailbox di un attore cresce inaspettatamente, state probabilmente inviando messaggi di un tipo che l’attore non conosce. Potete includere nel pattern matching una clausola case jolly (_) che analizza ed elabora i messaggi per scoprire chi sta infastidendo il vostro attore.

Gli attori nel dettaglio

Ora che conosciamo le basi del concetto di attore e abbiamo visto come viene usato in Scala, mettiamolo al lavoro. Nello specifico, mettiamolo al lavoro facendogli tagliare i capelli. Il problema del barbiere addormentato [SleepingBarberProblem] fa parte di un insieme di situazioni ipotetiche appositamente architettate dagli informatici per illustrare questioni di concorrenza e sincronizzazione.

Il problema è questo: un ipotetico salone in cui lavora un solo barbiere è dotato di una singola sedia da barbiere, e di tre sedie in cui i clienti possono aspettare di tagliarsi i capelli. Se non ci sono clienti, il barbiere dorme. Quando arriva un cliente, il barbiere si sveglia per tagliargli i capelli. Se il barbiere è occupato a tagliare i capelli quando un cliente arriva, il cliente si accomoda su una sedia libera. Se non ci sono sedie libere, il cliente se ne va.

Di solito, il problema del barbiere addormentato viene risolto con semafori e mutex, ma abbiamo strumenti migliori a nostra disposizione. Vediamo subito quali sono le diverse entità modellabili come attori: il barbiere, chiaramente, così come i clienti. Persino il negozio potrebbe essere modellato come un attore; anche se stiamo inviando messaggi, non è necessario che esista una comunicazione verbale tra due entità del mondo reale rappresentate come attori.

Cominciamo con i clienti del barbiere addormentato, dato che hanno le responsabilità più semplici.

// esempi/cap-9/sleepingbarber/customer.scala

package sleepingbarber

import scala.actors.Actor
import scala.actors.Actor._

case object Haircut

class Customer(val id: Int) extends Actor {
  var shorn = false

  def act() = {
    loop {
      react {
        case Haircut => {
          shorn = true
          println("[c] il cliente " + id + " è stato servito")
        }
      }
    }
  }
}

Per la maggior parte, questo dovrebbe sembrarvi piuttosto familiare: dichiariamo il package nel quale collocheremo il codice, importiamo codice dal package scala.actors e definiamo una classe che estende Actor. Ci sono alcuni dettagli che vale la pena di sottolineare, comunque.

Prima di tutto, c’è la nostra dichiarazione case object Haircut. Un pattern comune quando si lavora con gli attori in Scala è quello di usare un case object per rappresentare un messaggio senza dati interni. Se per esempio volessimo includere l’ora in cui il taglio di capelli è stato completato, useremmo una case class. Dichiariamo Haricut in questo punto del codice perché è un tipo di messaggio che verrà inviato solamente ai clienti.

Notate anche che stiamo memorizzando un singolo bit di stato mutabile in ogni istanza di Customer: se al cliente sono stati tagliati i capelli opppure no. Nel loro ciclo interno, tutti i clienti attendono un messaggio di tipo Haircut e, appena lo ricevono, impostano il valore booleano shorn a true. Customer usa il metodo asincrono react per rispondere ai messaggi in arrivo. Se avessimo bisogno di restituire il risultato della elaborazione del messaggio useremmo receive, ma questo non è necessario, perciò durante il procedimento risparmiamo thread e memoria dietro le quinte.

Procediamo con il barbiere, ora. Dato che c’è un solo barbiere, avremmo potuto crearlo usando il metodo factory actor menzionato in precedenza. Per facilitare il collaudo, invece, definiamo una nostra classe Barber.

// esempi/cap-9/sleepingbarber/barber.scala

package sleepingbarber

import scala.actors.Actor
import scala.actors.Actor._
import scala.util.Random

class Barber extends Actor {
  private val random = new Random()

  def helpCustomer(customer: Customer) {
    if (self.mailboxSize >= 3) {
      println("[b] non ci sono abbastanza sedie, mando via il cliente " + customer.id)
    } else {
      println("[b] taglio i capelli al cliente " + customer.id)
      Thread.sleep(100 + random.nextInt(400))
      customer ! Haircut
    }
  }

  def act() {
    loop {
      react {
        case customer: Customer => helpCustomer(customer)
      }
    }
  }
}

Il cuore della classe Barber somiglia molto a quello della classe Customer. Effettuiamo un ciclo sul metodo react, aspettando l’arrivo di un particolare tipo di oggetto. Per mantenere il ciclo breve e leggibile, invochiamo il metodo helpCustomer quando un nuovo cliente arriva dal barbiere. In quel metodo, controlliamo la dimensione della mailbox, che ci serve per rappresentare le “sedie” su cui i clienti in attesa si possono accomodare; potremmo utilizzare un’istanza di Queue come coda interna gestita dalla classe Barber o dalla classe Shop, ma perché preoccuparci dato che la mailbox di ogni attore è già una coda?

Se tre o più clienti sono in coda, ignoriamo semplicemente il messaggio, che viene poi scartato dalla mailbox del barbiere. Altrimenti, simuliamo un ritardo semi-casuale (100 millisecondi come minimo) per il tempo necessario a tagliare i capelli a un cliente, poi inviamo un messaggio Haricut a quel cliente. (Se stessimo simulando uno scenario reale, ovviamente rimuoveremmo l’invocazione di Thread.sleep() e permetteremmo al nostro barbiere di lavorare il più velocemente possibile.)

Come prossima cosa, scriviamo una semplice classe per rappresentare il salone.

// esempi/cap-9/sleepingbarber/shop.scala

package sleepingbarber

import scala.actors.Actor
import scala.actors.Actor._

class Shop extends Actor {
  val barber = new Barber()
  barber.start

  def act() {
    println("[s] il salone è aperto")

    loop {
      react {
        case customer: Customer => barber ! customer
      }
    }
  }
}

Ora tutto questo dovrebbe sembrarvi molto familiare. Ogni negozio crea un nuovo barbiere e lo fa partire, stampa un messaggio annunciando che il negozio è aperto e attende in un ciclo che arrivino i clienti. Quando arriva un’istanza di Customer, il cliente viene mandato dal barbiere. Ora notiamo un beneficio inaspettato degli attori: ci permettono di descrivere la logica applicativa concorrente in termini facili da capire. “Manda il cliente dal barbiere” ha perfettamente senso, molto più senso di “avvisa il barbiere, sblocca il mutex attorno alle sedie dei clienti, incrementa il numero di sedie libere”, e così via. Gli attori ci avvicinano al nostro dominio.

Infine, creiamo un programma per effettuare una simulazione.

// esempi/cap-9/sleepingbarber/barbershop-simulator.scala

package sleepingbarber

import scala.actors.Actor._
import scala.collection.{immutable, mutable}
import scala.util.Random

object BarbershopSimulator {
  private val random = new Random()
  private val customers = new mutable.ArrayBuffer[Customer]()
  private val shop = new Shop()

  def generateCustomers {
    for (i <- 1 to 20) {
      val customer = new Customer(i)
      customer.start()
      customers += customer
    }

    println("[!] genero " + customers.size + " clienti")
  }

  // i clienti arrivano a intervalli di tempo casuali
  def trickleCustomers {
    for (customer <- customers) {
      shop ! customer
      Thread.sleep(random.nextInt(450))
    }
  }

  def tallyCuts {
    // attende la terminazione delle rimanenti azioni concorrenti
    Thread.sleep(2000)

    val shornCount = customers.filter(c => c.shorn).size
    println("[!] oggi sono stati serviti " + shornCount + " clienti")
  }

  def main(args: Array[String]) {
    println("[!] inizio la simulazione del salone da barbiere")
    shop.start()

    generateCustomers
    trickleCustomers
    tallyCuts

    System.exit(0)
  }
}

Dopo aver “aperto il negozio”, generiamo un certo numero di oggetti Customer, assegnando un identificatore numerico a ognuno di loro e memorizzando il lotto in un ArrayBuffer. Poi facciamo “entrare uno alla volta” i clienti, inviandoli come messaggi al negozio e fermandoci per un certo periodo semi-casuale di tempo tra un passo del ciclo e l’altro. Alla fine della giornata simulata, calcoliamo il numero di clienti a cui sono stati tagliati i capelli filtrando i clienti il cui valore booleano shorn interno è stato impostato a true, poi stampiamo la dimensione della sequenza risultante.

Compilate il codice ed eseguitelo dalla directory sleepingbarber come segue:

fsc *.scala
scala -classpath . sleepingbarber.BarbershopSimulator

In tutto il codice, abbiamo usato alcuni prefissi nei messaggi a video con abbreviazioni per indicare le classi che hanno stampato i messaggi. Quando osserviamo un’esecuzione di esempio del nostro simulatore, è facile vedere da dove provengono i messaggi.

[!] inizio la simulazione del salone da barbiere
[s] il salone è aperto
[!] genero 20 clienti
[b] taglio i capelli al cliente 1
[b] taglio i capelli al cliente 2
[c] il cliente 1 è stato servito
[c] il cliente 2 è stato servito
[b] taglio i capelli al cliente 3
[c] il cliente 3 è stato servito
[b] taglio i capelli al cliente 4
[b] taglio i capelli al cliente 5
[c] il cliente 4 è stato servito
[b] taglio i capelli al cliente 6
[c] il cliente 5 è stato servito
[b] taglio i capelli al cliente 7
[c] il cliente 6 è stato servito
[b] non ci sono abbastanza sedie, mando via il cliente 8
[b] taglio i capelli al cliente 9
[c] il cliente 7 è stato servito
[b] non ci sono abbastanza sedie, mando via il cliente 10
[c] il cliente 9 è stato servito
[b] taglio i capelli al cliente 11
[b] taglio i capelli al cliente 12
[c] il cliente 11 è stato servito
[b] taglio i capelli al cliente 13
[c] il cliente 12 è stato servito
[b] taglio i capelli al cliente 14
[c] il cliente 13 è stato servito
[b] non ci sono abbastanza sedie, mando via il cliente 15
[b] non ci sono abbastanza sedie, mando via il cliente 16
[b] non ci sono abbastanza sedie, mando via il cliente 17
[b] taglio i capelli al cliente 18
[c] il cliente 14 è stato servito
[b] taglio i capelli al cliente 19
[c] il cliente 18 è stato servito
[b] taglio i capelli al cliente 20
[c] il cliente 19 è stato servito
[c] il cliente 20 è stato servito
[!] oggi sono stati serviti 15 clienti

Com’è prevedibile, vedrete che l’uscita di ogni esecuzione sarà leggermente diversa. Ogni volta che il barbiere ci mette un po’ di più a tagliare i capelli dando tempo a diversi clienti di entrare, le “sedie” (la mailbox del barbiere, che funziona come coda) si riempiono e i nuovi clienti se ne vanno.

Naturalmente, per gli esempi semplici come questo valgono le solite avvertenze. Per citarne una, è possibile che il nostro esempio non sia abbastanza casuale, in particolare se i valori casuali vengono recuperati a distanza di un millisecondo l’uno dall’altro. Questa è una conseguenza del modo in cui la JVM genera numeri casuali, e può servire come promemoria per ricordare di fare attenzione alla casualità nei programmi concorrenti. Potreste anche voler sostituire l’invocazione di sleep in tallyCuts con un segnale più chiaro che i vari attori nel sistema hanno finito di fare il proprio lavoro, magari trasformando BarbershopSimulator in un attore e inviandogli messaggi che indicano il completamento.

Provate a modificare il codice per introdurre più clienti, tipi di messaggio aggiuntivi, ritardi differenti, o a rimuovere completamente la casualità. Se siete programmatori multithread esperti, potreste provare a scrivere la vostra soluzione del problema, giusto per confrontarla con quella proposta. Siamo pronti a scommettere che un’implementazione con gli attori in Scala sarà più chiara e più facile da mantenere.

Attori efficaci

Per ottenere il massimo dagli attori, ecco alcune cose da ricordare. Prima di tutto, notate che esistono diversi metodi che potete usare per ottenere diversi tipi di comportamento dai vostri attori. La tabella seguente dovrebbe aiutarvi a comprendere quando usare ogni metodo.

Tabella 9.1. I metodi degli attori.

MetodoValore di ritornoDescrizione

act

Unit

Metodo astratto a livello radice nella gerarchia di un attore. Tipicamente, contiene uno dei metodi che seguono.

receive

Il risultato della elaborazione del messaggio

Si blocca fino a quando non viene ricevuto un messaggio di un tipo corrispondente.

receiveWithin

Il risultato della elaborazione del messaggio

Come receive, ma si sblocca dopo il numero di millisecondi specificato.

react

Nothing

Richiede meno costi aggiuntivi (in termini di thread) rispetto a receive.

reactWithin

Nothing

Come react, ma si sblocca dopo il numero di millisecondi specificato.

Tipicamente, vorrete usare react ogni volta che potete. Se avete bisogno dei risultati della elaborazione di un messaggio (cioè, avete bisogno di una risposta sincrona all’invio di un messaggio a un attore) usate la variante receiveWithin per ridurre le vostre possibilità di rimanere bloccati all’infinito su un attore che è rimasto incastrato.

Un’altra strategia per mantenere asincrono il vostro codice basato sugli attori è quella di usare i futuri (in inglese, futures). Un futuro è un oggetto segnaposto per un valore che non è ancora stato restituito da un processo asincrono. Potete inviare un messaggio a un attore con il metodo !!; una variante di questo metodo vi permette di passare una funzione parziale che viene applicata al valore futuro. Come potete vedere nell’esempio seguente, recuperare un valore da un’istanza di Future è tanto semplice quanto invocare il suo metodo apply. Notate che il recupero del valore da un’istanza di Future è un’operazione bloccante.

// esempi/cap-9/future-script.scala
import scala.actors.Futures._

val eventually = future(5 * 42)
println(eventually())

Ogni attore nel vostro sistema dovrebbe avere responsabilità chiare. Non usate gli attori per compiti generici che fanno largo uso dello stato. Invece, ragionate come i registi: quali sono i ruoli distinti nello “script” della vostra applicazione, e qual è la minima quantità di informazione di cui ogni attore ha bisogno per fare il proprio lavoro? Date a ogni attore solo un paio di responsabilità e usate i messaggi (di solito nella forma di classi case o di oggetti case) per delegare quelle responsabilità ad altri attori.

Non esistate a effettuare copie di dati quando scrivete codice basato su attori. Più sfruttate l’immutabilità nel vostro progetto, meno è probabile che il vostro stato finisca per assumere valori inattesi. Più comunicate via messaggi, meno dovete preoccuparvi della sincronizzazione. L’uso di tutti quei messaggi e di tutte quelle variabili immutabili vi potrà sembrare eccessivamente costoso, ma, con l’odierna disponibilità di hardware, sostenere costi aggiuntivi di memoria in cambio di chiarezza e predicibilità sembra più che legittimo per la maggior parte delle applicazioni.

Infine, sappiate capire quando gli attori non sono la soluzione migliore. Solo perché gli attori sono un modo fantastico di gestire la concorrenza in Scala non significa che essi siano l’unico modo, come vedremo fra poco. L’impiego di strumenti tradizionali come thread e lock potrebbe essere più adatto in situazioni critiche con frequenti operazioni di scrittura, per le quali il costo aggiuntivo di un approccio a messaggi sarebbe troppo elevato. La nostra esperienza ci dice che potete creare un prototipo di soluzione concorrente progettandolo puramente in termini di attori, per poi tracciarne un profilo in modo da scoprire quali parti della vostra applicazione potrebbero beneficiare di un approccio differente.

La concorrenza tradizionale in Scala: thread ed eventi

Sebbene gli attori siano un modo fantastico di gestire la concorrenza, non sono l’unico modo per farlo in Scala. Essendo Scala interoperabile con Java, i concetti di programmazione concorrente della JVM che potrebbero già esservi noti sono ancora applicabili.

Thread una tantum

Per cominciare, Scala vi offre un modo comodo di eseguire un blocco di codice in un nuovo thread.

// esempi/cap-9/threads/by-block-script.scala

new Thread { println("questo verrà eseguito in un nuovo thread") }

Il package scala.concurrent contiene un costrutto simile, sotto forma del metodo spawn dell’oggetto ops, per eseguire un blocco in maniera asincrona.

// esempi/cap-9/threads/spawn-script.scala

import scala.concurrent.ops._

object SpawnExample {
  def main(args: Array[String]) {
    println("questo verrà eseguito in maniera sincrona")

    spawn {
      println("questo verrà eseguito in maniera asincrona")
    }
  }
}

Usare java.util.concurrent

Se avete familiarità con il venerabile package java.util.concurrent, lo troverete altrettanto facile (o difficile, a seconda del vostro punto di vista) da usare in Scala. Tramite Executors creeremo un pool di thread per eseguire una semplice classe che implementa l’interfaccia Runnable per rappresentare istanze eseguibili dai thread; la classe identifica il thread in cui viene eseguita.

// esempi/cap-9/threads/util-concurrent-script.scala

import java.util.concurrent._

class ThreadIdentifier extends Runnable {
  def run {
    println("ciao dal Thread " + currentThread.getId)
  }
}

val pool = Executors.newFixedThreadPool(5)

for (i <- 1 to 10) {
  pool.execute(new ThreadIdentifier)
}

Come è prassi nella programmazione concorrente in Java, il metodo run è il punto di partenza per le classi eseguite da un thread. Ogni volta che il nostro pool esegue un nuovo ThreadIdentifier, il suo metodo run viene invocato. Un’occhiata all’uscita seguente ci dice che siamo eseguendo cinque thread nel pool, con identificatori che vanno da 9 a 13.

ciao dal Thread 9
ciao dal Thread 10
ciao dal Thread 11
ciao dal Thread 12
ciao dal Thread 13
ciao dal Thread 9
ciao dal Thread 11
ciao dal Thread 10
ciao dal Thread 10
ciao dal Thread 13

Questo, naturalmente, scalfisce solo la superficie di quanto è disponibile in java.util.concurrent. Scoprirete che tutto ciò che sapete sull’approccio multithread di Java si applica ancora in Scala. In più, sarete in grado di portare a termine gli stessi compiti usando meno codice, migliorandone la manutenibilità e incrementando la vostra produttività.

Eventi

Gli attori e i thread non sono gli unici strumenti per realizzare programmi concorrenti. La concorrenza basata su eventi, sotto forma di un particolare approccio all’I/O asincrono o non bloccante (NIO), è diventata una strategia privilegiata per implementare server che devono scalare verso migliaia di client simultanei. Evitando la tradizionale relazione uno-a-uno tra thread e client, questo modello di concorrenza espone eventi che si verificano quando hanno luogo particolari condizioni (per esempio, quando i dati di un client vengono ricevuti da una socket di rete). Tipicamente, il programmatore assocerà un metodo di callback a ogni evento rilevante per il programma.

Nonostante il package java.nio offra una varietà di primitive utili per l’I/O non bloccante (buffer, canali, &c.), è necessario uno sforzo ulteriore per assemblare un programma concorrente basato su eventi a partire da quelle semplici primitive. Entra Apache MINA, realizzato su Java NIO e descritto sul proprio sito come “un framework per applicazioni di rete che aiuta gli utenti a sviluppare facilmente applicazioni a prestazioni e scalabilità elevate” (si veda [MINA]).

Sebbene MINA possa essere più facile da usare rispetto alle librerie NIO predefinite di Java, ci siamo abituati ad alcune comodità di Scala che non sono proprio disponibili in MINA. La libreria open source Naggati (si veda [Naggati]) aggiunge a MINA uno strato progettato per agevolare i programmatori Scala e che, secondo il suo autore, “rende più facile filtrare protocolli usando uno stile sequenziale”. In sostanza, Naggati è un DSL per analizzare protocolli di rete sfruttando le potenti caratteristiche di I/O non bloccante di MINA dietro le quinte.

Useremo Naggati per scrivere le fondamenta di un server email SMTP. Per semplificare le cose, ci occuperemo solo di due comandi SMTP, HELO e QUIT: il primo identifica un client, il secondo chiude la sessione di comunicazione con il client.

Saremo onesti con noi stessi e manterremo una serie di test, facilitati dalla libreria Specs per lo sviluppo guidato dal comportamento (si veda la sezione Specs nel capitolo 14).

// esempi/cap-9/smtpd/src/test/scala/com/programmingscala/smtpd/SmtpDecoderSpec.scala

package com.programmingscala.smtpd

import java.nio.ByteOrder
import net.lag.naggati._
import org.apache.mina.core.buffer.IoBuffer
import org.apache.mina.core.filterchain.IoFilter
import org.apache.mina.core.session.{DummySession, IoSession}
import org.apache.mina.filter.codec._
import org.specs._
import scala.collection.{immutable, mutable}

object SmtpDecoderSpec extends Specification {
  private var fakeSession: IoSession = null
  private var fakeDecoderOutput: ProtocolDecoderOutput = null
  private var written = new mutable.ListBuffer[Request]

  def quickDecode(s: String): Unit = {
    Codec.decoder.decode(fakeSession, IoBuffer.wrap(s.getBytes), fakeDecoderOutput)
  }

  "SmtpRequestDecoder" should {
    doBefore {
      written.clear()
      fakeSession = new DummySession
      fakeDecoderOutput = new ProtocolDecoderOutput {
        override def flush(nextFilter: IoFilter.NextFilter, s: IoSession) = {}
        override def write(obj: AnyRef) = written += obj.asInstanceOf[Request]
      }
    }

    "riconoscere HELO" in {
      quickDecode("HELO client.example.org\n")
      written.size mustEqual 1
      written(0).command mustEqual "HELO"
      written(0).data mustEqual "client.example.org"
    }

    "riconoscere QUIT" in {
      quickDecode("QUIT\n")
      written.size mustEqual 1
      written(0).command mustEqual "QUIT"
      written(0).data mustEqual null
    }
  }
}

Dopo aver preparato l’ambiente per l’esecuzione di ogni test, la nostra specifica esercita i due comandi SMTP che ci interessano. Il blocco doBefore viene eseguito prima di ogni test, garantendo che la sessione e il buffer di uscita fittizi siano correttamente inizializzati. In ogni test, passiamo una stringa di ingresso proveniente da un ipotetico client al nostro Codec non ancora implementato, poi verifichiamo che la richiesta risultante (Request, una classe case) contenga i campi command e data corretti. Dato che il comando QUIT non richiede ulteriori informazioni dal client, controlliamo semplicemente che data sia null.

Con i nostri test in posizione, implementiamo un codec (codificatore/decodificatore) di base per SMTP.

// esempi/cap-9/smtpd/src/main/scala/com/programmingscala/smtpd/Codec.scala

package com.programmingscala.smtpd

import org.apache.mina.core.buffer.IoBuffer
import org.apache.mina.core.session.{IdleStatus, IoSession}
import org.apache.mina.filter.codec._
import net.lag.naggati._
import net.lag.naggati.Steps._

case class Request(command: String, data: String)
case class Response(data: IoBuffer)

object Codec {
  val encoder = new ProtocolEncoder {
    def encode(session: IoSession, message: AnyRef, out: ProtocolEncoderOutput) = {
      val buffer = message.asInstanceOf[Response].data
      out.write(buffer)
    }

    def dispose(session: IoSession): Unit = {
      // operazione nulla, richiesta dal tratto ProtocolEncoder
    }
  }

  val decoder = new Decoder(readLine(true, "ISO-8859-1") { line =>
    line.split(' ').first match {
      case "HELO" => state.out.write(Request("HELO", line.split(' ')(1))); End
      case "QUIT" => state.out.write(Request("QUIT", null)); End
      case _ => throw new ProtocolError("Riga di richiesta malformata: " + line)
    }
  })
}

Per prima cosa, definiamo Request come una classe case in cui memorizzare i dati di richiesta quando arrivano. Poi specifichiamo in encoder la porzione di codifica del nostro codec, che si occupa semplicemente di scrivere i dati in uscita. Definiamo anche un metodo dispose (senza riempirlo) per rispettare il contratto del tratto ProtocolEncoder.

Il decodificatore è la parte che ci interessa veramente. readRequest legge una riga, prende la prima parola in quella riga e usa il pattern matching su di essa per riconoscere i comandi SMTP. In caso di un comando HELO, usiamo anche la stringa seguente su quella stessa riga. I risultati vengono messi in un oggetto Request e scritti in uscita su state. Come potete immaginare, state memorizza i nostri progressi durante il processo di analisi.

Pur essendo semplice, l’esempio appena visto mostra quanto sia facile analizzare protocolli con Naggati. Ora che abbiamo un codificatore funzionante, possiamo combinare Naggati e MINA con gli attori per creare un server.

Prima di tutto, ci serviranno alcune noiose righe di configurazione per avviare il nostro server SMTP.

// esempi/cap-9/smtpd/src/main/scala/com/programmingscala/smtpd/Main.scala

package com.programmingscala.smtpd

import net.lag.naggati.IoHandlerActorAdapter
import org.apache.mina.filter.codec.ProtocolCodecFilter
import org.apache.mina.transport.socket.SocketAcceptor
import org.apache.mina.transport.socket.nio.{NioProcessor, NioSocketAcceptor}
import java.net.InetSocketAddress
import java.util.concurrent.{Executors, ExecutorService}
import scala.actors.Actor._

object Main {
  val listenAddress = "0.0.0.0"
  val listenPort = 2525

  def setMaxThreads = {
    val maxThreads = (Runtime.getRuntime.availableProcessors * 2)
    System.setProperty("actors.maxPoolSize", maxThreads.toString)
  }

  def initializeAcceptor = {
    var acceptorExecutor = Executors.newCachedThreadPool()
    var acceptor =
      new NioSocketAcceptor(acceptorExecutor, new NioProcessor(acceptorExecutor))
    acceptor.setBacklog(1000)
    acceptor.setReuseAddress(true)
    acceptor.getSessionConfig.setTcpNoDelay(true)
    acceptor.getFilterChain.addLast("codec",
            new ProtocolCodecFilter(smtpd.Codec.encoder, smtpd.Codec.decoder))
    acceptor.setHandler(
            new IoHandlerActorAdapter(session => new SmtpHandler(session)))
    acceptor.bind(new InetSocketAddress(listenAddress, listenPort))
  }

  def main(args: Array[String]) {
    setMaxThreads
    initializeAcceptor
    println("smtpd: avviato e in ascolto su " + listenAddress + ":" + listenPort)
  }
}

Per assicurarci di sfruttare al massimo le istanze di attori nel nostro server, abbiamo impostato la proprietà di sistema actors.maxPoolSize al doppio del numero di processori disponibili sulla nostra macchina. Poi abbiamo inizializzato un’istanza di NioSocketAcceptor, un meccanismo chiave di MINA che viene utilizzato per accettare nuove connessioni dai client. Le ultime tre righe di questa configurazione sono critiche, dato che mettono al lavoro il nostro codificatore, dicono ad acceptor di gestire le richieste usando un particolare oggetto e mettono il server in attesa di nuove connessioni sulla porta 2525 (i server SMTP reali sono in ascolto sulla porta privilegiata 25).

Il particolare oggetto appena menzionato è un attore racchiuso in un’istanza di IoHandlerActorAdapter, uno strato di collegamento tra gli attori Scala e MINA fornito da Naggati. Questa è la parte del nostro server che risponde al client. Ora che sappiamo quello che il client sta dicendo, grazie al decodificatore, sappiamo anche cosa rispondere!

// esempi/cap-9/smtpd/src/main/scala/com/programmingscala/smtpd/SmtpHandler.scala

package com.programmingscala.smtpd

import net.lag.naggati.{IoHandlerActorAdapter, MinaMessage, ProtocolError}
import org.apache.mina.core.buffer.IoBuffer
import org.apache.mina.core.session.{IdleStatus, IoSession}
import java.io.IOException
import scala.actors.Actor
import scala.actors.Actor._
import scala.collection.{immutable, mutable}

class SmtpHandler(val session: IoSession) extends Actor {
  start

  def act = {
    loop {
      react {
        case MinaMessage.MessageReceived(msg) =>
            handle(msg.asInstanceOf[smtpd.Request])
        case MinaMessage.SessionClosed => exit()
        case MinaMessage.SessionIdle(status) => session.close
        case MinaMessage.SessionOpened => reply("220 localhost Tapir SMTPd 0.1\n")

        case MinaMessage.ExceptionCaught(cause) => {
          cause.getCause match {
            case e: ProtocolError => reply("502 Errore: " + e.getMessage + "\n")
            case i: IOException   => reply("502 Errore: " + i.getMessage + "\n")
            case _                => reply("502 Errore sconosciuto\n")
          }
          session.close
        }
      }
    }
  }

  private def handle(request: smtpd.Request) = {
    request.command match {
      case "HELO" => reply("250 Salve " + request.data + "\n")
      case "QUIT" => reply("221 Hasta la vista, baby\n"); session.close
    }
  }

  private def reply(s: String) = {
    session.write(new smtpd.Response(IoBuffer.wrap(s.getBytes)))
  }

}

Possiamo immediatamente riconoscere lo stesso schema che abbiamo visto nei precedenti esempi di attori in questo capitolo: un ciclo attorno a un blocco react che usa il pattern matching con un insieme limitato di casi. In SmtpHandler, tutti questi casi sono eventi scatenati da MINA. Per esempio, MINA ci invierà MinaMessage.SessionOpened quando un client si connette e MinaMessage.SessionClosed quando un client si disconnette.

Il caso che ci interessa di più è quello di MinaMessage.MessageReceived. Ci viene passato un familiare oggetto Request con ogni nuovo messaggio valido ricevuto, e possiamo usare il pattern matching sul campo command per intraprendere l’azione appropriata. Quando il client dice HELO, possiamo rispondere con una breve nota di conferma. Quando il client dice QUIT, lo salutiamo e lo disconnettiamo.

Ora che abbiamo messo insieme tutti i pezzi, proviamo a intrattenere una conversazione con il nostro server.

[al3x@jaya ~]$ telnet localhost 2525
Trying ::1...
Connected to localhost.
Escape character is '^]'.
220 localhost Tapir SMTPd 0.1
HELO jaya.local
250 Salve jaya.local
QUIT
221 Hasta la vista, baby
Connection closed by foreign host.

Una conversazione breve, di sicuro, ma il nostro server funziona! Ora, cosa succede se gli inviamo qualcosa di inaspettato?

[al3x@jaya ~]$ telnet localhost 2525
Trying ::1...
Connected to localhost.
Escape character is '^]'.
220 localhost Tapir SMTPd 0.1
HELO jaya.local
250 Salve jaya.local
BAD COMMAND
502 Errore: Riga di richiesta malformata: BAD COMMAND
Connection closed by foreign host.

Ben fatto. È stata un’ottima cosa essersi presi la briga di estrarre quelle eccezioni quando il nostro attore SmtpHandler riceve un evento MinaMessage.ExceptionCaught.

Naturalmente, ciò che abbiamo costruito gestisce solo l’inizio e la fine di una conversazione SMTP completa. Come esercizio, provate a implementare i comandi rimanenti. Oppure, per arrivare subito a qualcosa di molto simile a ciò che abbiamo realizzato qui, date un’occhiata al progetto open source Mailslot (si veda [Mailslot]).

Riepilogo, e poi?

Abbiamo imparato come costruire applicazioni concorrenti scalabili e robuste usando la libreria di attori di Scala, che evita i problemi degli approcci tradizionali basati sull’accesso sincronizzato a uno stato mutabile condiviso. Abbiamo anche mostrato che il potente modello a thread predefinito di Java è utilizzabile in Scala. Infine, abbiamo imparato come combinare gli attori con il potente framework NIO Apache MINA e con Naggati per sviluppare da zero un server di rete asincrono e basato su eventi in poche righe di codice.

Il prossimo capitolo esaminerà il supporto predefinito di Scala per lavorare con XML.

© 2008–9 O’Reilly Media
© 2009–10 Giulio Piancastelli per la traduzione italiana