Parte III — Message passing e attori · Capitolo 13

Il modello ad attori

~50 min di lettura12 widget interattivi

In questo capitolo

  1. Perché il modello ad attori
  2. Il modello di Carl Hewitt
  3. Le tre primitive: send, create, become
  4. Semantica: macro-step, incapsulamento, fairness
  5. L'event loop degli attori
  6. Implementazioni: panoramica e ActorFoundry
  7. Pekko (ex Akka): attori sulla JVM
  8. Erlang: processi, mailbox e pattern matching
  9. Pattern di messaggistica e actor idioms
  10. Spaghetti asincrono: future, stashing e LSC
  11. Il problema della proattività
  12. Supervisione e fault tolerance
  13. Delivery semantics e ordinamento dei messaggi
  14. Testing degli attori
  15. Errori comuni e best practice
  16. Verifica le tue conoscenze

1. Perché il modello ad attori

Il modello ad attori nasce come alternativa al multi-threading tradizionale per la programmazione concorrente e distribuita. L'idea centrale è che tutto nel sistema sia un attore: un'entità autonoma, puramente reattiva, che comunica solo tramite scambio asincrono di messaggi.

Nella lezione, il Prof. Ricci sottolinea come il modello ad attori sia diventato sempre più rilevante nel panorama mainstream. Framework come Akka (ora Pekko sulla JVM), Erlang, HTML5 Web Workers e Dart Isolates sono esempi concreti di adozione di questo modello. La ragione principale è che gli attori offrono una via d'uscita dai problemi classici della programmazione concorrente: race condition, deadlock, complessità della sincronizzazione manuale.

Idea chiave

Un attore è come un "oggetto concorrente": incapsula stato e flusso di controllo. A differenza degli oggetti classici dell'OOP, dove lo stato è protetto ma il metodo viene eseguito nel thread del chiamante, un attore possiede un proprio flusso di controllo logico e processa i messaggi uno alla volta.

Il Prof. Ricci ricorda come Alan Kay, padre della OOP, intendesse originariamente gli oggetti proprio come entità che comunicano tramite message passing. Il modello ad attori realizza compiutamente questa visione: ogni attore ha un identificatore unico, una mailbox (coda di messaggi) e un comportamento che determina come reagire ai messaggi ricevuti.

Thread classici

  • Memoria condivisa
  • Lock / synchronized
  • Race condition frequenti
  • Deadlock possibili
  • Debug complesso

Attori

  • Nessuno stato condiviso
  • Solo message passing
  • Race condition eliminate
  • Deadlock gestiti dal modello
  • Isolamento naturale
Per l'esame

Il modello Actor è una delle alternative al multi-threading che potete adottare nel secondo assignment. È importante capire la differenza fondamentale: mentre nei thread la comunicazione avviene tramite memoria condivisa (con i relativi problemi di race condition e lock), negli attori la comunicazione avviene esclusivamente tramite messaggi asincroni.

L'idea centrale è: everything is an actor. Ogni entità computazionale è un attore, con:

Il modello in 90 secondi

Nella lezione di ripasso il messaggio chiave è condensato così: un actor è uno stato incapsulato che reagisce solo ai messaggi. Niente accesso diretto allo stato altrui, niente controllo condiviso, solo comunicazione asincrona.

Idea chiave

Actor = mailbox + stato + comportamento. Il controllo vive nel messaggio che arriva, non in una variabile condivisa.

ConcettoIn pratica
sendInvia un messaggio senza bloccare il mittente
createIstanzia un nuovo actor con un comportamento preciso
becomeCambia il comportamento per il prossimo messaggio
macro-stepUn handler si completa prima di passare al messaggio successivo
send(other, "ping", self(), "ciao")
// invio asincrono: il mittente continua subito
public void ping(ActorName caller) {
  send(caller, "alive", self());
  become(new WaitingForAck());
}

Gli actor semplificano il ragionamento sulle race, ma diventano difficili da leggere se spezzi la logica in troppi handler senza un protocollo chiaro.

Attenzione

Un handler non deve bloccare. Se ti serve attesa, buffering o chaining: sposta la logica nel protocollo, non in un while infinito.

2. Il modello di Carl Hewitt

Il modello Actor fu originariamente introdotto da Carl Hewitt e colleghi al MIT negli anni '70, nel contesto dell'intelligenza artificiale. Successivamente sviluppato da Gul Agha e Akinori Yonezawa negli anni '80 e '90 come unificazione fra OOP e concorrenza: la cosiddetta Concurrent Object-Oriented Programming. Il punto di partenza è la domanda: "come possiamo modellare il calcolo concorrente in modo matematico?"

La risposta è sorprendentemente semplice: l'attore è il primitivo universale del calcolo concorrente digitale. Il modello Actor è una teoria matematica che tratta gli attori come i primitivi universali della computazione concorrente. Tutto — incluse le strutture di controllo tradizionali (if, while, procedure) — può essere modellato come pattern di scambio di messaggi tra attori. Ogni entità nel sistema è un attore con un identificatore univoco e una mailbox (coda di messaggi).

  1. 1973 — Carl Hewitt introduce il modello Actor con "A Universal Modular ACTOR Formalism", come teoria matematica per il calcolo concorrente
  2. 1977 — Hewitt pubblica "Viewing Control Structures as Patterns of Passing Messages" (Journal of AI)
  3. 1986 — Gul Agha formalizza il modello nella tesi "Actors: A Model of Concurrent Computation in Distributed Systems"
  4. 1987 — Yonezawa e Tokoro curano "Concurrent Object Oriented Programming" (MIT Press); nello stesso anno nasce Erlang in Ericsson per applicazioni telecom
  5. 1990 — Agha pubblica su CACM: il modello Actor come unificazione di OOP e concorrenza
  6. 2009 — Akka: framework actor industrial-grade per la JVM
  7. 2000+ — Erlang, Scala/Akka (poi Pekko), HTML5 Web Workers, Dart isolates e sistemi cloud-native portano gli attori nel mainstream
Idea chiave

Il modello Actor è fortemente legato all'idea originale di OOP secondo Alan Kay: il punto fondamentale dell'OOP non erano le classi o l'ereditarietà, ma il message passing ("The key point was message passing"). Gli oggetti sono entità autonome che comunicano scambiandosi messaggi — esattamente come gli attori. Con una differenza: un attore incapsula stato, comportamento e un flusso di controllo logico; gli oggetti classici non incapsulano il flusso di controllo.

Un attore è dunque un'entità computazionale che incapsula stato, comportamento e un flusso di controllo logico. A differenza degli oggetti classici, gli attori sono autonomi e reattivi: lavorano solo quando ricevono un messaggio.

flowchart LR
    subgraph A1 [Attore A]
        S1[(Stato)] & C1[Comportamento] & ID1[ID]
    end
    subgraph A2 [Attore B]
        S2[(Stato)] & C2[Comportamento] & ID2[ID]
    end
    A1 -->|messaggio asincrono| A2
    A2 -->|messaggio asincrono| A1
    

I tre pilastri del modello

Un attore è definito da tre elementi essenziali:

flowchart LR
    A1[Attore 1] -->|send msg| A2[Attore 2]
    A2 -->|send reply| A1
    A1 -->|create| A3[Attore 3]
    A3 -->|become| A3b[Attore 3 con nuovo behavior]
    

Nonostante le origini accademiche, il modello Actor ha trovato ampia adozione industriale: Erlang per sistemi telecom estremamente robusti, Akka/Pekko per applicazioni enterprise su JVM, HTML5 Web Workers per il parallelismo nel browser, Dart isolates, e molti altri framework. È considerato un'alternativa valida al multithreading tradizionale per costruire sistemi concorrenti scalabili.

Nota del redattore

Lo state explorer illustra il ciclo di vita di un attore: dalla creazione all'attesa di messaggi (ATTIVO), all'esecuzione atomica del gestore (ELABORAZIONE), fino alla terminazione. Un attore è puramente reattivo: può solo rispondere a messaggi, non iniziare azioni autonomamente.

3. Le tre primitive: send, create, become

Il modello ad attori definisce solo tre primitive per comporre il comportamento di un attore. Ogni altra costruzione (sincronizzazione, ordinamento, pattern) è derivabile da queste tre.

PrimitivaAzioneAnalogia sequenziale
sendInvia asincronamente un messaggio a un attore specificatoEquivalente alla chiamata di procedura (procedure invocation)
createCrea un attore con il comportamento specificatoEquivalente all'astrazione di procedura (procedure abstraction)
becomeSpecifica un nuovo comportamento (e stato locale) per il prossimo messaggioAssegnazione di stato: dota l'attore di comportamento history-sensitive
Idea chiave

Il professore sottolinea l'eleganza del modello: con solo tre primitive si può esprimere qualsiasi computazione concorrente. send è l'equivalente concorrente della chiamata di procedura; create è l'equivalente della definizione di procedura; become permette la storia (cambio di stato nel tempo).

sendInvia un msg asincrono a un attore
createCrea un nuovo attore con un dato behavior
becomeCambia il behavior per il prossimo messaggio

Send è per la programmazione concorrente ciò che l'invocazione di procedura è per la programmazione sequenziale: il mattoncino fondamentale. Create è l'analogo dell'astrazione di procedura: permette di definire nuovi attori. Become permette all'attore di cambiare il proprio comportamento (e quindi il proprio stato locale) per il messaggio successivo, dando agli attori una history-sensitive behaviour necessaria per modellare oggetti mutabili condivisi.

Send — invia un messaggio asincrono a un attore specificato. È per la programmazione concorrente ciò che l'invocazione di procedura è per la programmazione sequenziale: l'operazione fondamentale di interazione.

In Pekko si usa l'operatore tell o !: actorRef ! messaggio.

Create — crea un nuovo attore con un comportamento specificato. È per la programmazione concorrente ciò che l'astrazione di procedura è per la programmazione sequenziale.

In Pekko: context.spawn(behavior, "nome").

Become — specifica un nuovo comportamento che l'attore userà per rispondere al prossimo messaggio. Questo dà agli attori la capacità di avere un comportamento history-sensitive, necessario per modellare dati mutabili condivisi.

In Pekko: context.become(nuovoBehavior) restituito dalla receive.

Un aspetto cruciale è che un attore può comunicare solo con attori di cui conosce l'identificatore. Questi identificatori possono essere scambiati all'interno dei messaggi, permettendo la costruzione di topologie di comunicazione dinamiche.

Un'altra osservazione importante: a differenza degli oggetti classici dell'OOP, gli attori incapsulano anche il controllo — un oggetto normale non ha un flusso di controllo proprio (è passivo), mentre un attore è intrinsecamente attivo. Tuttavia, gli attori sono disaccoppiati dai thread fisici del sistema operativo: sono entità logiche. Come le goroutine di Go, una piattaforma attori può gestire milioni di attori su pochi thread fisici.

Nota del redattore

Le tre primitive sono state enunciate da Hewitt nel 1973 e rimangono valide oggi. Ogni framework moderno le implementa con nomi e sintassi diversi, ma la semantica è sempre quella.

4. Semantica: macro-step, incapsulamento, fairness

La semantica del modello Actor si fonda su cinque aspetti chiave:

PrincipioDescrizione
Comportamento puramente reattivoUn attore lavora solo quando riceve un messaggio. Nessun messaggio = bloccato.
Incapsulamento dello statoUn attore non può accedere direttamente allo stato interno di un altro attore.
Macro-step semanticsUna volta ricevuto un messaggio, la computazione viene eseguita completamente prima di servire un altro messaggio (run-to-completion).
FairnessUn messaggio inviato a un attore viene prima o poi recapitato e processato.
Location transparencyPer inviare un messaggio a un attore basta conoscere la sua identità, non la sua locazione fisica.

Macro-step semantics (run-to-completion)

Quando un attore riceve un messaggio, l'intero handler viene eseguito completamente prima di passare al messaggio successivo. Questo elimina alla radice il problema delle race condition sullo stato interno. Gli handler devono però avere un comportamento non bloccante — l'unico punto di blocco è gestito dal ciclo eventi dell'attore.

flowchart LR
    A[Attore] --> B[mailbox: msg1, msg2, msg3]
    B --> C[estrai msg1]
    C --> D[esegui handler]
    D --> E[msg1 completato]
    E --> F[estrai msg2]
    F --> G[esegui handler]
    G --> H[msg2 completato]
    

Le conseguenze della macro-step semantics sono profonde:

Per l'esame

La macro-step semantics è cruciale: evita le race condition perché l'handler viene eseguito atomicamente rispetto agli altri messaggi. Tuttavia, complica la programmazione perché un handler non può bloccarsi in attesa di un altro messaggio — bloccherebbe tutto l'attore.

Attenzione

La semantica macro-step è una benedizione per la correttezza ma una maledizione per la leggibilità. Il Prof. Ricci cita il problema dello "asynchronous spaghetti" (Ricci & Santi, AGERE! 2012): la logica applicativa viene spezzata in una serie di handler disconnessi, ciascuno dei quali reagisce a un singolo messaggio.

La macro-step semantics è identica a quella delle architetture event-driven: GUI management, JavaScript con callback, Node.js. Tutte condividono lo stesso modello: un ciclo eventi che estrae un evento, esegue il gestore fino al completamento, poi passa al prossimo evento.

Incapsulamento dello stato

Un attore non può accedere direttamente allo stato interno di un altro attore. L'unico modo per interagire con un attore è inviargli un messaggio. Questo incapsulamento è molto più forte di quello dell'OOP classico: non solo i dati, ma anche il flusso di controllo è incapsulato. L'attore decide quando e come processare un messaggio, e può persino rimandare messaggi (stashing).

Fairness

La fairness è un altro punto cruciale. Hewitt voleva un modello efficace anche nei sistemi distribuiti, quindi stabilì che: se un attore invia un messaggio, questo sarà eventualmente recapitato alla mailbox del destinatario. Non c'è garanzia sul tempo, né sull'ordine. Questa assunzione è volutamente debole per essere realizzabile in ambienti distribuiti reali (diversamente da UDP, che non garantisce neppure la consegna).

Per l'esame

La fairness in message delivery è un assunto fondamentale del modello Actor: un messaggio inviato viene eventualmente recapitato ed elaborato. Non ci sono garanzie temporali né di ordinamento. La frase "eventually delivered" è un concetto chiave da ricordare.

Location transparency

La location transparency permette di inviare messaggi a un attore conoscendo solo la sua identità (actor ref), senza sapere se risiede sullo stesso processo, sulla stessa macchina, o su un nodo remoto. Questo è ciò che rende il modello Actor naturalmente adatto a sistemi distribuiti: lo stesso meccanismo funziona per attori locali e remoti.

Per l'esame

La location transparency è il pilastro che permette ad Akka/Pekko Cluster di funzionare: un ActorRef si comporta allo stesso modo sia che l'attore sia locale o remoto. Il professore ha sottolineato più volte questo concetto durante la lezione.

State Explorer: ciclo di vita con become

5. L'event loop degli attori

L'implementazione pratica del modello attore si basa su un event loop implicito o esplicito. L'architettura di controllo di un attore è un message loop (implicitamente gestito dal framework):

Event loop implicito

loop {
  msg <- waitForMsg()            // attesa bloccante (solo qui)
  handler <- selectHandler(msg)  // dispatch del messaggio
  execute(handler)               // esecuzione atomica
}

Esempi: ActorFoundry, Akka/Pekko, Vert.x, Web Workers.

Event loop esplicito

Il programmatore scrive il loop di ricezione e usa pattern matching. Esempio: Erlang.

Idea chiave

La macro-step semantics è condivisa da entrambi gli approcci: un handler viene eseguito completamente prima di ricevere il messaggio successivo. L'unico punto di blocco è la receive. Questo implica che gli handler devono avere un comportamento non bloccante — la stessa sfida della never-blocking rule vista per gli event loop.

  • Niente race condition (grazie alla macro-step semantics).
  • Ragionamento semplificato sul programma.
  • Disaccoppiamento tra concorrenza logica (N attori) e fisica (M thread OS).
  • Migliaia/centinaia di migliaia di attori possono coesistere su pochi thread.
  • Problema dello "spaghetti asincrono".
  • Difficoltà a esprimere comportamenti pro-attivi.
  • Complesso coordinare attori che devono attendersi prima di procedere.

Il widget seguente mostra il ciclo di vita di un attore visto dal punto di vista del message loop:

6. Implementazioni: panoramica e ActorFoundry

Il modello Actor ha diverse implementazioni, con varianti semantiche significative. Le più importanti presentate a lezione sono tre: Erlang (linguaggio nativo), Akka/Pekko (framework JVM), e ActorFoundry (framework accademico Java).

Erlang è un linguaggio funzionale sviluppato in Ericsson per applicazioni di telecomunicazione. La sua VM (BEAM) gestisce processi leggeri come entità logiche, non legate a thread OS. Un programma Erlang è un insieme di funzioni; i processi (attori) vengono creati con spawn, comunicano con ! (send) e ricevono con receive su pattern matching.

counter(Sum) ->
    receive
        {inc} ->
            counter(Sum + 1);
        {getValue, Pid} ->
            Pid ! {count_value, Sum},
            counter(Sum)
    end.

Erlang è noto per l'estrema efficienza: centinaia di migliaia di processi su un singolo host.

Akka è un framework industriale per Java e Scala, che supersede la vecchia libreria Scala Actors. Offre attori tipati (Akka Typed), cluster, sharding, persistence, e un ricco ecosistema. Gli attori Akka sono definiti tramite Behaviors e ActorContext.

public class MyUntypedActor extends UntypedActor {
    LoggingAdapter log = Logging.getLogger(getContext().system(), this);
    public void onReceive(Object message) throws Exception {
        if (message instanceof String) {
            log.info("Received String message: {}", message);
            getSender().tell("received");
        } else unhandled(message);
    }
}

Akka (oggi nel fork Apache Pekko) è la piattaforma di riferimento per il corso, ed è quella che il professore usa in tutti gli esempi pratici.

ActorFoundry è un framework accademico Java sviluppato dal gruppo di Agha all'Università dell'Illinois. Usa annotazioni @message per definire gli handler. Supporta ottimizzazioni come Kilim e constraint di sincronizzazione locale.

public class PingActor extends Actor {
    ActorName otherPinger;
    @message
    public void start(ActorName other) {
        otherPinger = other;
        send(otherPinger, "ping", self(), "...");
    }
}

È principalmente usato per scopi di ricerca e didattici.

Explicit vs Implicit receive

Una differenza fondamentale tra i framework è come gestiscono il message loop:

ActorFoundry: esempio concreto in Java

ActorFoundry è un framework accademico basato su Java, sviluppato dal gruppo di Gul Agha all'Università dell'Illinois (UIUC). Include ottimizzazioni e idee da altri lavori di ricerca come Kilim. Mostra in modo chiaro e minimalista le primitive fondamentali del modello: il programmatore estende la classe Actor e definisce i metodi che saranno invocati alla ricezione di un messaggio.

Il codice seguente mostra un classico ping-pong fra attori:

Analizziamo gli elementi chiave:

In PingBoot, l'attore di boot crea due attori PingActor e avvia il protocollo inviando a pinger1 il messaggio "start" con il riferimento a pinger2. I due attori si scambiano messaggi "ping" e "alive", stampando il tutto sullo stdout tramite l'attore standard stdout.

Nota del redattore

La notazione send(otherPinger, "ping", self(), ...) mostra la differenza fondamentale rispetto alla chiamata a metodo OOP: otherPinger.ping(...) sarebbe una chiamata bloccante con trasferimento del controllo; send(...) è invece un invio asincrono di un dato nella mailbox del destinatario, senza trasferimento del flusso di controllo.

7. Pekko (ex Akka): attori sulla JVM

Nota del redattore

Il framework di riferimento del corso è Apache Pekko, fork open-source di Akka: quando Lightbend (l'azienda dietro Akka) ha cambiato licenza in Business Source License, la comunità ha forkato il cuore tecnologico sotto l'ombrello Apache. La trascrizione della lezione riporta il nome come "Pecco": in questo capitolo normalizziamo ovunque a Pekko. Pekko è compatibile al 100% con Akka: gli stessi moduli, le stesse API (package org.apache.pekko), la stessa documentazione. Tutti gli esempi preesistenti per Akka funzionano con Pekko senza modifiche.

Akka è il framework per attori più maturo a livello industriale per l'ecosistema JVM. Scritto in Scala ma utilizzabile da entrambi i linguaggi, offre un ricco set di funzionalità tra cui tolleranza ai guasti stile Erlang ("let it crash"), clustering, stashing, e pattern di supervisione e di messaggistica avanzati. Supporta sia untyped actors (Java) che typed actors (Scala).

import akka.actor.UntypedActor;
import akka.event.Logging;
import akka.event.LoggingAdapter;

public class MyUntypedActor extends UntypedActor {
  LoggingAdapter log = Logging.getLogger(getContext().system(), this);

  public void onReceive(Object message) throws Exception {
    if (message instanceof String) {
      log.info("Received String message: {}", message);
      getSender().tell("received");
    } else
      unhandled(message);
  }
}

L'attore estende UntypedActor e sovrascrive onReceive. Il metodo riceve un Object generico e usa pattern matching tramite instanceof per determinare il tipo di messaggio. getSender() restituisce il riferimento al mittente per poter rispondere.

import akka.actor.Actor
import akka.actor.Props
import akka.event.Logging

class MyActor extends Actor {
  val log = Logging(context.system, this)
  def receive = {
    case "test" => log.info("received test")
    case _      => log.info("received unknown message")
  }
}

La versione Scala è più concisa grazie al pattern matching nativo del linguaggio. Il metodo receive definisce una partial function che matcha i messaggi in arrivo. context.system dà accesso al sistema attori. Il pattern matching è esaustivo (il caso _ cattura tutti i messaggi sconosciuti).

Approfondimento

Akka/Pekko implementa l'architettura a event loop incapsulato (encapsulated event loop): il programmatore definisce solo gli handler (come onReceive o receive), e il framework gestisce implicitamente il ciclo di attesa dei messaggi. L'attore è quindi implicitamente reattivo — la reattività è una proprietà dell'architettura, non del codice scritto dal programmatore.

L'API typed di Pekko

Le dipendenze necessarie per iniziare con Pekko includono il modulo core typed e il modulo di logging basato su SLF4J. La versione typed dell'API è un'evoluzione importante: nell'API classica (untyped) si poteva mandare qualsiasi messaggio a qualsiasi attore, anche messaggi che l'attore non era in grado di gestire. Con l'API typed, ogni attore dichiara il tipo T dei messaggi che accetta, e il compilatore garantisce la correttezza a tempo di compilazione.

ConcettoRuolo
ActorSystemRoot del sistema ad attori; runtime che gestisce tutti gli attori
Behavior<T>Definisce il comportamento di un attore in risposta a messaggi di tipo T
ActorRef<T>Riferimento tipizzato a un attore; si usa per inviargli messaggi
ActorContext<T>Contesto dell'attore: permette di spawnare figli, fermarsi, accedere al sistema

Actor system, behavior e actor ref

Il sistema ad attori ha una struttura gerarchica. L'ActorSystem è la radice: crea gli attori di primo livello. Ogni attore può a sua volta essere padre di altri attori, formando un albero di supervisione. Questo è ispirato al modello di Erlang, dove la filosofia è "Let It Crash": gli attori possono fallire, ma il padre (supervisore) decide come reagire.

flowchart TD
    S[ActorSystem] --> A1[Attore A]
    S --> A2[Attore B]
    A1 --> C1[Attore C - figlio di A]
    A1 --> C2[Attore D - figlio di A]
    A2 --> E1[Attore E - figlio di B]
    

Un behavior in Pekko è il comportamento dell'attore: una funzione che prende un messaggio e un contesto e restituisce il prossimo behavior. È importante capire che il prossimo behavior non deve essere per forza lo stesso: un attore può cambiare comportamento dinamicamente in risposta ai messaggi.

Creazione di un attore in Pekko

import org.apache.pekko.actor.typed.*;
import org.apache.pekko.actor.typed.javadsl.*;

public class HelloWorld {
    public static Behavior<String> create() {
        return Behaviors.setup(context -> {
            return Behaviors.receiveMessage(message -> {
                System.out.println("Hello " + message);
                return Behaviors.same();
            });
        });
    }

    public static void main(String[] args) {
        ActorSystem<String> system = ActorSystem.create(HelloWorld.create(), "hello-system");
        system.tell("World!");
    }
}

L'actor ref è il riferimento tramite cui si interagisce con un attore. È parametrizzato sul tipo T dei messaggi accettati: ActorRef<T>. Se si prova a inviare un messaggio di tipo sbagliato, il programma non compila.

Per l'esame

Distinguere bene: Behavior<T> è cosa l'attore fa; ActorRef<T> è dove inviare messaggi; ActorContext<T> è il contesto per operazioni come spawn, stop, watch.

Esempio Ping-Pong in Pekko

Il Prof. Ricci mostra l'esempio classico per illustrare l'interazione tra attori: il protocollo Ping-Pong. Due attori si scambiano messaggi: Pinger e Ponger. Il messaggio Ping contiene un campo replyTo con l'actor ref a cui rispondere.

public interface PingProtocol { }
public static final class Ping implements PingProtocol {
    public final ActorRef<PongProtocol> replyTo;
    public Ping(ActorRef<PongProtocol> replyTo) {
        this.replyTo = replyTo;
    }
}
public interface PongProtocol { }
public static final class Pong implements PongProtocol { }

public static Behavior<PingProtocol> pingerBehavior() {
    return Behaviors.setup(context -> Behaviors.receiveMessage(msg -> {
        if (msg instanceof Ping p) {
            p.replyTo.tell(new Pong());
            return Behaviors.same();
        }
        return Behaviors.unhandled();
    }));
}

public static Behavior<PongProtocol> pongerBehavior() {
    return Behaviors.setup(context -> Behaviors.receiveMessage(msg -> {
        if (msg instanceof Pong) {
            System.out.println("Ricevuto Pong!");
            return Behaviors.same();
        }
        return Behaviors.unhandled();
    }));
}

Il pattern è chiaro: il messaggio Ping "incapsula" il protocollo di risposta portando con sé il riferimento a chi deve rispondere. Questo evita la necessità di un meccanismo di sincronizzazione separato. Il Prof. Ricci sottolinea che questo è il modo idiomatico di fare request-response nel mondo ad attori: via messaggio con campo replyTo, non via chiamata bloccante.

Message-driven protocol

Invece di usare ask (che restituisce una Future), il modo più idiomatico e puro è includere l'actor ref del mittente nel messaggio stesso. Questo mantiene il sistema completamente asincrono e reattivo.

8. Erlang: processi, mailbox e pattern matching

Erlang è il linguaggio che ha portato il modello attore nella produzione industriale: probabilmente il linguaggio di programmazione concorrente più robusto e utilizzato dall'industria, insieme ad Ada. Sviluppato in Ericsson a partire dal 1987 per costruire applicazioni telecom, è un linguaggio funzionale che si basa su un modello a processi leggeri (non thread OS) che comunicano via message passing asincrono. Il cuore di Erlang è la BEAM (Bogdan/Björn's Erlang Abstract Machine), una macchina virtuale che implementa un modello ad attori nativo: i processi Erlang sono concettualmente identici agli attori.

flowchart LR
    subgraph BEAM[BEAM Virtual Machine]
        P1[Processo 1
mailbox] P2[Processo 2
mailbox] P3[Processo N
mailbox] end P1 -->|invia messaggio| P2 P2 -->|invia messaggio| P3 P3 -->|invia messaggio| P1

Caratteristiche principali

Processi vs Thread

CaratteristicaThread OSProcesso Erlang
Costo creazioneDecine di microsecondi~5 microsecondi
Numero massimoMigliaiaCentinaia di migliaia
Memoria per processoMB (stack + TCB)~300 parole
SchedulingPreemptivo (OS)Cooperativo (BEAM)
IsolamentoMemoria condivisaNessuna memoria condivisa
Attenzione

Il Prof. Ricci avverte: non confondete i processi Erlang con i processi del sistema operativo. I processi Erlang sono entità virtuali gestite dalla BEAM, molto più leggeri e veloci da creare. Ogni processo ha una propria mailbox e non condivide memoria con altri processi.

Nota del redattore

Joe Armstrong, il creatore di Erlang, ha definito il linguaggio come "il miglior modo per scrivere sistemi concorrenti, distribuiti e fault-tolerant". La BEAM supporta centinaia di migliaia di processi con costo di creazione di pochi microsecondi.

Primitive fondamentali

Erlang è un linguaggio funzionale: un programma descrive una serie di funzioni, con pattern matching per determinare quale clausola eseguire. Le variabili iniziano con la lettera maiuscola (come in Prolog). Non esistono variabili globali.

La primitiva spawn crea un nuovo processo (attore) specificando modulo, nome funzione e parametri (o una fun anonima). Restituisce un PID (Process IDentifier) che identifica univocamente il processo. L'operatore ! invia un messaggio a un processo: Pid ! Message. La receive selettiva estrae un messaggio dalla mailbox del processo usando pattern matching:

% Creazione processo
Pid = spawn(fun () -> account(0) end).
% Invio messaggio
Pid ! Message.
% Ricezione con pattern matching
receive
  Pattern1 [ when Guard1 ] -> Expression1;
  Pattern2 [ when Guard2 ] -> Expression2;
  ...
end

Il processo si blocca finché non trova un messaggio nella coda che matcha un pattern con guardia verificata.

Esempio: area server

% Esempio Erlang: area server (dalle slide)
-module(area_server0).
-export([loop/0]).

loop() ->
    receive
        {rectangle, Width, Ht} ->
            io:format("Area of rectangle is ~p~n", [Width * Ht]),
            loop();
        {circle, R} ->
            io:format("Area of circle is ~p~n", [3.14159 * R * R]),
            loop();
        Other ->
            io:format("I don't know what ~p is~n", [Other]),
            loop()
    end.

% Nell'interprete:
% 1> Pid = spawn(fun area_server0:loop/0).
% 2> Pid ! {rectangle, 3, 4}.
% Area of rectangle is 12

Notare che: (1) ogni caso della receive termina con ; tranne l'ultimo che termina con .; (2) ogni caso chiama ricorsivamente loop() per rimettersi in ascolto del messaggio successivo; (3) il pattern Other cattura qualsiasi messaggio non riconosciuto.

Esempio Counter in Erlang

Il Prof. Ricci mostra come in Erlang anche un semplice contatore sia un processo. Non esiste memoria condivisa: tutto è un processo. Lo stato (il valore corrente) vive nel parametro della funzione ricorsiva.

-module(counter).
-export([start/0]).

start() -> loop(0).

loop(Sum) ->
    receive
        {inc} ->
            loop(Sum + 1);
        {getValue, Pid} ->
            Pid ! {count_value, Sum},
            loop(Sum)
    end.

% 1> Pid = spawn(counter, start, []).
% 2> Pid ! {inc}.
% 3> Pid ! {getValue, self()}.

Esempio: account bancario

account(Balance) ->
  receive
    {deposit, Amount, Whom} -> Whom ! {deposit_receipt, Amount},
                               account(Balance + Amount);
    {balance, Whom} -> Whom ! {balance, Balance},
                       account(Balance);
    {withdrawal, Amount, Whom} when Amount > Balance ->
                               Whom ! overdraft, account(Balance);
    {withdrawal, Amount, Whom} -> Whom ! {withdrawal_receipt, Amount},
                               account(Balance - Amount)
  end.

L'esempio con la for comprehension mostra come creare decine di migliaia di processi in pochi millisecondi, misurando il tempo di spawn:

-module(processes).
-export([max/1]).

max(N) ->
    Max = erlang:system_info(process_limit),
    io:format("Maximum allowed processes:~p~n",[Max]),
    statistics(runtime),
    statistics(wall_clock),
    L = for(1, N, fun() -> spawn(fun() -> wait() end) end),
    {_, Time1} = statistics(runtime),
    {_, Time2} = statistics(wall_clock),
    ...

% Output per N=20000:
% Process spawn time=5.5 (9.4) microseconds
"Everything is a process"

In Erlang, non esistono oggetti o dati mutabili condivisi. Anche un semplice contatore è un processo con la propria mailbox. Questa filosofia radicale elimina ogni possibilità di race condition.

Per l'esame

Il professore sottolinea che Erlang non è un linguaggio accademico ma è stato usato (ed è ancora usato) per fare software molto robusto in ambito telecom. La BEAM è una delle macchine virtuali concorrenti più collaudate al mondo.

9. Pattern di messaggistica e actor idioms

Il modello ad attori prevede diversi pattern di interazione, ciascuno con i propri casi d'uso. Il Prof. Ricci li presenta in ordine crescente di complessità.

Fire-and-forget (tell)

L'attore invia un messaggio e non si aspetta una risposta. È il caso più semplice e comune: si usa actorRef.tell(messaggio) o l'operatore !. Esempio: inviare un Increment a un contatore senza curarsi del risultato.

Request-response con replyTo

L'attore mittente include nel messaggio un campo replyTo: ActorRef<Risposta>. Il destinatario usa questo riferimento per inviare la risposta. È il pattern idiomatico del mondo ad attori: preserva l'asincronia e non richiede blocking.

Ask pattern

Pekko fornisce anche l'operatore ask che restituisce una Future. Questo è utile quando si vuole interagire con un attore da codice non-actor (es. da un main) o quando serve una risposta ma non si vuole implementare il protocollo manualmente.

import static org.apache.pekko.actor.typed.javadsl.AskPattern.ask;
import java.time.Duration;
import java.util.concurrent.CompletionStage;

CompletionStage<String> risposta = ask(
    actorRef,
    replyTo -> new RichiediDati(replyTo),
    Duration.ofSeconds(5),
    system.scheduler()
);

risposta.thenAccept(valore -> {
    System.out.println("Ricevuto: " + valore);
});

Il Prof. Ricci fa notare che ask è utile ma "esce un po' dal modello puro". L'uso della Future introduce un meccanismo che non è nativo del modello ad attori. Tuttavia, quando usato con giudizio, risolve il problema dell'asynchronous spaghetti: si possono combinare più ask in sequenza con thenCompose o in parallelo con Promise.all.

Actor Idioms (Schumacher, AGERE! 2012)

Come ogni paradigma, anche il modello Actor ha i suoi pattern ricorrenti. I principali sono:

Messaging Patterns (V. Vernon)

Timeline storica del message passing

  1. 1970 — Brinch-Hansen introduce l'async message passing per l'RC4000.
  2. 1971 — Bob Balzer introduce il concetto di communication port.
  3. 1973 — Carl Hewitt introduce il modello Actor all'MIT.
  4. 1974 — Dijkstra introduce le guarded commands.
  5. 1978 — Hoare formalizza CSP (Communicating Sequential Processes).
  6. 1986 — Gul Agha pubblica "Actors: A Model of Concurrent Computation".
  7. 1987 — Ericsson sviluppa Erlang per telecomunicazioni.
  8. 2007 — Go language (Google): goroutine, canali, select.
  9. Oggi — Akka/Pekko, Dart isolates, Web Workers, attori mainstream.

10. Spaghetti asincrono: future, stashing e LSC

La macro-step semantics semplifica il ragionamento sugli attori ma introduce un problema serio: la logica applicativa viene frammentata in un insieme non strutturato di handler/callback, ciascuno dei quali reagisce a un evento specifico. Quando un flusso di lavoro richiede una sequenza di passi, ogni passo diventa un handler separato. Il professore chiama questo il problema degli "spaghetti asincroni" (asynchronous spaghetti), rifacendosi a [RS-12].

Esempio: sin(x) * cos(y)

L'esempio classico dalle slide: un attore deve valutare l'espressione sin(x)*cos(y) delegando i calcoli a due attori specializzati (sinActor e cosActor). Con la semantica macro-step, l'attore principale non può bloccarsi in attesa dei risultati. Deve:

  1. Inviare x a sinActor e y a cosActor.
  2. Ricevere il risultato da sinActor in un handler.
  3. Ricevere il risultato da cosActor in un altro handler.
  4. Quando entrambi i risultati sono arrivati, calcolare il prodotto.

Questo frammenta la logica in tre handler separati, perdendo la struttura sequenziale naturale dell'algoritmo: ogni risposta richiede un handler diverso, e bisogna tracciare lo stato per sapere se l'altra risposta è già arrivata. La letteratura [RS-12] documenta approfonditamente questo problema.

Soluzioni proposte

Un future è un proxy per un risultato inizialmente sconosciuto. Nell'Actor model, un future è rappresentato da un attore con tre stati: senza valore e senza clienti in attesa; clienti in coda fino a quando il valore è disponibile; valore disponibile che notifica i clienti in attesa.

Il future può essere passato ad altri attori prima di essere risolto, aumentando il parallelismo. Linguaggi come E e AmbientTalk offrono supporto nativo ai future/promise. Akka/Pekko fornisce Future e Promise come pattern fondamentali.

Akka introduce il meccanismo di stashing: i messaggi ricevuti possono essere accantonati temporaneamente in una coda separata (stash) e recuperati successivamente (unstashAll). Questo permette di gestire protocolli con stati senza frammentare la logica.

class ActorWithProtocol extends Actor with Stash {
  def receive = {
    case "open" =>
      unstashAll()
      context.become({
        case "write" => // do writing...
        case "close" => unstashAll(); context.unbecome()
        case msg => stash()
      }, discardOld = false)
    case msg => stash()
  }
}

ActorFoundry implementa le Local Synchronization Constraints (LSC): condizioni di abilitazione/disabilitazione specificate per gli handler tramite annotazioni. Una condizione @Disable definisce quando un messaggio NON può essere processato; i messaggi disabilitati vengono accodati in una save queue per elaborazione successiva.

@Disable(messageName = "put")
public Boolean disablePut(Integer x) {
  if (bufferReady) { return (tail == bufferSize); }
  else return true;
}
Per l'esame

Le LSC (Local Synchronization Constraints) separano il quando un messaggio viene processato dal come. Un messaggio disabilitato viene messo in una save queue per essere processato in seguito.

Lo stash in dettaglio

Lo stash è una coda implicita separata dalla mailbox principale:

Il pattern classico è: in uno stato transitorio, i messaggi non gestibili vengono stashati; quando si arriva allo stato giusto (o scade un timer), si fa unstashAll() e si riprocessano.

// Pattern Akka: stash per gestire un protocollo a stati
Behaviors.receive { (ctx, msg) =>
  msg match {
    case "open" =>
      unstashAll()
      Behaviors.receiveMessage {
        case "write" => // scrivi...
        case "close" =>
          unstashAll()
          context.unbecome()
        case _ => stash()
      }
    case _ => stash()
  }
}
Idea chiave

Il problema degli spaghetti asincroni colpisce più in generale tutte le architetture event-driven basate su callback: programmazione Web (JavaScript), GUI management, Node.js, e attori. Alcune soluzioni includono promise pipeline e costrutti linguistici che permettono di scrivere codice dall'aspetto sequenziale mantenendo l'elaborazione asincrona.

RPC-like messaging

Un pattern comune è simulare chiamate RPC in un modello puramente asincrono. Il client invia una richiesta e poi controlla i messaggi in arrivo. Se arriva la risposta, la processa; altrimenti, bufferizza i messaggi non correlati per elaborazione futura. Questo protocollo è implementabile con le primitive di base ma è complesso — per questo i framework offrono supporto diretto, come la primitiva call in ActorFoundry. Il pattern Future è alla base della comunicazione RPC-like nel modello actor: un attore-future fa da proxy per un risultato non ancora disponibile e, quando il valore viene calcolato, notifica tutti gli attori in attesa.

11. Il problema della proattività

Gli attori sono entità puramente reattive: lavorano solo quando ricevono un messaggio. Ma come si modella un comportamento proattivo, orientato al raggiungimento di obiettivi tramite un piano di azioni? Il professore dedica una parte significativa della lezione a questo problema, che ritiene concettualmente importante. Le soluzioni classiche includono: dividere l'attore in più attori interagenti, oppure il self-sending di messaggi.

L'esempio concreto: un attore deve calcolare e stampare i primi 100 numeri, ma deve poter ricevere un messaggio stop che interrompe la stampa. Un attore puramente reattivo non può iniziare da solo la stampa — deve aspettare un messaggio che glielo richieda. La soluzione proposta dal professore è il self-sending: l'attore invia messaggi a se stesso per procedere incrementalmente nel lavoro.

// Messaggio di start
send me, "iterat_print", i, end

// Handler per iterat_print
if (not stopped)
  print(i)
  if (i < end)
    send me, "iterat_print", i+1, end

// Handler per stop
stopped = true

Il pattern è semplice ma profondo: ogni stampa è un passo atomico gestito da un messaggio. Se arriva un messaggio stop prima del completamento, l'attore smette di auto-inviarsi messaggi e la stampa si ferma. L'attore rimane reattivo (ogni messaggio è una reazione), ma l'auto-invio crea un effetto proattivo complessivo.

Nota del redattore

Il simulatore mostra l'alternanza fra il messaggio di auto-invio (che fa progredire la stampa) e la possibilità di inserire uno stop in qualsiasi momento. Il professore sottolinea che questa è una soluzione elegante ma parziale; la letteratura [RS-12] propone astrazioni più avanzate per integrare proattività e reattività a livello di linguaggio.

Nota del redattore

Il self-sending di messaggi è una tecnica potente che ricorda il postMessage degli event loop: l'attore programma la sua prossima attività mandando un messaggio alla propria mailbox. Questo rispetta la natura reattiva (c'è sempre un messaggio che innesca l'azione) pur ottenendo un comportamento ciclico/pro-attivo.

Il problema della proattività è affrontato anche dalla ricerca: esistono proposte per estendere il modello Actor con astrazioni che permettano di specificare comportamenti proattivi (piani, goal) mantenendo la reattività come meccanismo di interazione. Tuttavia, nel modello di base, il self-sending rimane la tecnica più diffusa e pragmatica.

12. Supervisione e fault tolerance

Uno degli aspetti più potenti del modello ad attori è il meccanismo di supervisione, ispirato a Erlang. L'idea è che gli attori falliscano (lanciando eccezioni), ma il padre-supervisore decide come reagire. Non si cerca di prevenire tutti i fallimenti: si accetta che accadano e si fornisce un meccanismo di recovery ("Let It Crash").

Lo state explorer seguente mostra il ciclo di vita completo di un attore, incluso lo stato di fallimento gestito dal supervisore:

Strategie di supervisione

Quando un attore figlio fallisce, il padre può applicare diverse strategie:

StrategiaComportamento
RestartRiavvia l'attore figlio: viene creato da capo con stato iniziale (quello definito in setup)
ResumeRiprende l'esecuzione del figlio mantenendo lo stato interno corrente (delicato, perché lo stato potrebbe essere corrotto)
StopFerma definitivamente l'attore figlio
EscalatePropaga il fallimento al supervisore del padre, che deciderà a sua volta

Il meccanismo di supervisione ha un effetto collaterale molto importante: la pulizia automatica delle risorse. Se un attore padre fallisce, tutti i figli vengono automaticamente fermati e le loro risorse rilasciate. Questo è molto diverso dal mondo dei thread, dove un thread orfano continua a vivere e il programmatore deve implementare meccanismi ad hoc per chiudere file, rilasciare lock, ecc.

Behaviors.setup(context -> {
    ActorRef<MyMsg> child = context.spawn(
        Behaviors.supervise(childBehavior())
            .onFailure(SupervisorStrategy.restart()),
        "child-actor"
    );
    return childBehavior();
});

Il Prof. Ricci menziona anche la funzione watch: un padre può osservare un figlio e ricevere una notifica ChildFailed o Terminated quando termina. Questo permette al padre di reagire alla terminazione (es. cleanup aggiuntivo o terminazione a catena del sistema).

13. Delivery semantics e ordinamento dei messaggi

Un tema importante toccato nella lezione riguarda le garanzie di consegna dei messaggi tra attori. Pekko (come Akka ed Erlang) adotta una semantica at-most-once: i messaggi possono essere persi (per crash, problemi di rete, ecc.), ma non saranno mai duplicati.

GaranziaDescrizione
At-most-onceOgni messaggio viene consegnato al massimo una volta. Possibile perdita, mai duplicazione.
At-least-onceOgni messaggio viene consegnato almeno una volta. Possibile duplicazione, mai perdita.
Exactly-onceOgni messaggio viene consegnato esattamente una volta. Massimo costo e complessità.
Attenzione

Nel modello di base, la send non dà alcuna garanzia sui tempi di consegna né sull'ordine di ricezione. Anche quando due send vengono eseguite in sequenza dallo stesso mittente, i messaggi potrebbero arrivare in ordine diverso al destinatario. Ogni forma di ordinamento deve essere realizzata tramite pattern di scambio messaggi.

Ordinamento

Le implementazioni pratiche come Pekko rafforzano il modello di base: viene garantito un ordinamento sender-receiver. Se A invia il messaggio M1 e poi M2 a B, M1 viene consegnato prima di M2. Tuttavia, non c'è garanzia di ordinamento tra coppie diverse: se A invia M1 a B e M2 a C, M2 potrebbe arrivare prima di M1. Per gestire la sincronizzazione e l'ordinamento locale, alcuni framework offrono inoltre vincoli di sincronizzazione locale (LSC in ActorFoundry) o lo stashing di Akka/Pekko, visti nella sezione 10.

Attenzione

Non date per scontato l'ordinamento globale dei messaggi. Il modello garantisce solo l'ordinamento per coppia sender-receiver. Se avete bisogno di ordinamento globale, dovete implementarlo voi usando pattern di messaggistica opportuni (es. sequencer actor).

14. Testing degli attori

Pekko fornisce un modulo di testing integrato con ScalaTest e JUnit. Il test kit permette di creare un ActorSystem di test, spawnare attori e verificare che i messaggi vengano scambiati correttamente.

import org.apache.pekko.actor.testkit.typed.javadsl.*;

@Test
public void testPingPong() {
    var testKit = ActorTestKit.create();
    var probe = testKit.createTestProbe();
    var pinger = testKit.spawn(pingerBehavior());

    pinger.tell(new Ping(probe.ref()));
    probe.expectMessageClass(Pong.class);
}

Il Prof. Ricci sottolinea l'importanza di testare gli attori, soprattutto perché la semantica asincrona può nascondere bug sottili. Il test kit di Pekko permette di: (1) spawnare attori in un ambiente controllato, (2) usare probe per catturare i messaggi in uscita, (3) impostare timeout per evitare test che si bloccano.

Nota del redattore

Il testing degli attori è più vicino al testing di integrazione che al testing unitario. Anche un singolo attore ha bisogno del runtime (ActorSystem) per funzionare. Il test kit fornisce un ambiente leggero per questo scopo.

15. Errori comuni e best practice

Il Prof. Ricci conclude la lezione con una serie di raccomandazioni pratiche su cosa evitare quando si sviluppa con gli attori.

Per l'esame

Queste raccomandazioni sono state sottolineate esplicitamente dal professore durante la lezione. Ricordatele per il progetto e per l'esame.

Cosa evitare

Pratica scorrettaSpiegazione
Bloccare gli handlerNon fare Thread.sleep(), I/O bloccante, o attese in un handler. Blocca l'event loop dell'attore.
Usare ask dappertuttoPreferire messaggi con replyTo per mantenere la purezza del modello ad attori.
Trattare la supervisione come validazioneLa supervisione serve per gestire fallimenti imprevisti, non per validare input errati.
Esporre il contestoNon esporre mai ActorContext all'esterno dell'attore. Rompe l'incapsulamento.
Attori monoliticiMeglio tanti piccoli attori che uno grande. Si possono creare milioni di attori: sfruttate questa caratteristica.

Raccomandazioni positive

Il Prof. Ricci ricorda che il modello ad attori consente di definire in modo esplicito il protocollo di interazione tra componenti, offre un modo strutturato per gestire fallimenti e ciclo di vita, e costituisce la base per passare dalla programmazione concorrente a quella distribuita (argomento del prossimo capitolo).

Verifica le tue conoscenze

Quali sono le tre primitive fondamentali del modello ad attori secondo Hewitt?

Send (inviare un messaggio asincrono), Create (creare un nuovo attore con un comportamento specificato), Become (cambiare il comportamento dell'attore per il prossimo messaggio).

Cosa garantisce la semantica macro-step (run-to-completion)?

Quando un attore riceve un messaggio, l'intero handler viene eseguito completamente prima di passare al messaggio successivo. Questo elimina le race condition sullo stato interno dell'attore.

Perché un actor riduce le race condition?

Perché lo stato resta incapsulato e il controllo passa solo attraverso messaggi asincroni, non tramite memoria condivisa.

Che differenza c'è tra tell e ask in Pekko?

tell invia un messaggio fire-and-forget (non si aspetta risposta). ask invia un messaggio e restituisce una Future per attendere la risposta. Il pattern idiomatico nel mondo ad attori è usare tell con un campo replyTo nel messaggio.

Qual è la semantica di consegna dei messaggi di default in Pekko/Akka?

At-most-once: i messaggi possono essere persi ma mai duplicati. Per quanto riguarda l'ordinamento, viene garantito l'ordine per coppia sender-receiver (se A manda M1 e poi M2 a B, M1 arriva prima di M2).

Cosa significa "Let It Crash" nel contesto della supervisione?

È la filosofia di Erlang adottata anche da Pekko: non cercare di prevenire ogni possibile fallimento, ma accettarlo e fornire meccanismi di recovery automatici attraverso la gerarchia di supervisione. Il padre dell'attore fallito decide se riavviarlo, riprenderlo o fermarlo.

Qual è la differenza tra un processo Erlang e un thread OS?

Un processo Erlang è un'entità logica gestita dalla BEAM, non un thread OS. La creazione costa ~5 microsecondi (contro decine di microsecondi per un thread), si possono creare centinaia di migliaia di processi, e ogni processo ha una memoria di circa 300 parole (contro MB per un thread). Non condividono memoria.

Quali sono i tre stati di una Promise?

Pending (creata, in attesa di completamento), Resolved/Fulfilled (completata con successo), Rejected (completata con errore). Una volta risolta o rifiutata, la Promise è settled e il suo stato non cambia più.

Che cosa devi evitare dentro un handler?

Blocchi lunghi, wait attive e loop infiniti: rompono la reattività del sistema.

Quali sono gli errori più comuni da evitare nella programmazione ad attori?

Bloccare gli handler (non fare mai blocking call), usare ask dappertutto (preferire messaggi con replyTo), usare la supervisione per validazione (serve per fallimenti imprevisti), esporre il contesto dell'attore all'esterno, creare attori monolitici (preferire tanti piccoli attori).

Qual è il primo passo per Assignment 3?

Tradurre la traccia in ruoli, messaggi, stato minimo e invarianti prima di scrivere implementazione.