17. Actor
MALDA offre il supporto nativo al modello ad actor per la programmazione concorrente. Gli actor sono unità di computazione indipendenti e isolate che comunicano esclusivamente tramite message passing asincrono. Ogni actor ha il proprio stato isolato e processa i messaggi in sequenza, garantendo una programmazione concorrente thread-safe senza meccanismi di locking espliciti.
17.1 Dichiarazione degli actor
Gli actor si dichiarano con la keyword actor, in modo analogo alle dichiarazioni di classe:
actor ActorName {
// Fields (state)
var field1 = initialValue;
var field2;
// Constructor (optional)
function ActorName(param1, param2) {
field1 = param1;
field2 = param2;
}
// Message handlers
on messageHandler1() {
// Handle message
}
on messageHandler2(param) {
// Handle message with parameter
}
// Default handler (optional)
on handle(msg) {
// Handles any message that doesn't match a specific handler
}
}
Componenti di un actor
- Campi: Variabili di stato dell'actor (isolate per istanza)
- Costruttore: Funzione di inizializzazione opzionale (stesso nome dell'actor)
- Message handler: Funzioni dichiarate con la keyword
onche processano i messaggi in arrivo - Handler di default: Un handler di nome
handleche riceve i messaggi quando nessun handler specifico corrisponde
17.2 Spawn degli actor
Gli actor si istanziano con l'espressione spawn:
var actorRef = spawn ActorName(arg1, arg2);
L'espressione spawn:
- Crea una nuova istanza di actor con stato isolato
- Avvia il loop di elaborazione dei messaggi dell'actor in un task separato
- Restituisce un
ActorReferenceche si può usare per inviare messaggi - Accetta opzionalmente gli argomenti del costruttore
Esempio
actor Counter {
var count = 0;
on increment() {
count = count + 1;
}
on get() {
print(count);
}
}
var counter = spawn Counter();
17.3 Invio dei messaggi
I messaggi si inviano agli actor con l'istruzione send nella sintassi call-style:
// Call-style syntax
send actorRef.handlerName(arg1, arg2);
// Call-style with callback
send actorRef.handlerName(arg1) then (result) {
// Handle reply value
};
Sintassi di invio dei messaggi
send target.handlerName(arg1, arg2, ...);- Invia un singolo messaggio all'handler indicato con argomenti posizionalisend target(arg1, arg2, ...);- Invia un messaggio senza specificare il nome dell'handler. Il messaggio viene instradato all'handlerhandle(se definito), oppure usa le regole di risoluzione dell'handler in base al payloadsend target.handlerName(args...) then (result) { ... };- Invia un messaggio e registra un callback da eseguire quando arriva la replysend target(args...) then (result) { ... };- Invia un messaggio senza nome di handler e registra un callbacksend target.handlerName(args...) then (result) { ... } timeout milliseconds;- Invia un messaggio con callback e timeout (il timeout scatta se non arriva alcuna reply)send target.handlerName(args...) then (result) { ... } timeout milliseconds catch (error) { ... };- Invia un messaggio con callback, timeout e error handler per i casi di timeout- La sintassi call-style invia esattamente un messaggio che contiene sia il nome dell'handler (se specificato) sia tutti gli argomenti
Esempio
actor Greeter {
var name;
function Greeter(actorName) {
name = actorName;
}
on greet() {
print($"Hello from {name}!");
}
on greetWith(message) {
print($"{name}: {message}");
}
}
var greeter = spawn Greeter("Alice");
send greeter.greet(); // Calls greet() handler
send greeter.greetWith("Hello, World!"); // Calls greetWith() handler with message
send greeter("Hello!"); // Calls handle() handler (if defined)
Dettagli del message passing
- I messaggi vengono inviati in modo asincrono (non bloccante)
- I messaggi vengono accodati nella mailbox dell'actor
- Gli actor processano i messaggi in sequenza (uno alla volta)
- Più actor possono inviare messaggi allo stesso actor in concorrenza
Request/Response con callback
Puoi inviare un messaggio e registrare un callback che verrà eseguito quando il destinatario risponde:
actor Worker {
on compute(value) {
var result = value * 2;
reply(result); // Sends a reply back to the sender
}
}
actor Coordinator {
on start() {
var worker = spawn Worker();
send worker.compute(21) then (result) {
print($"Coordinator: received result = {result}");
};
}
}
var coordinator = spawn Coordinator();
// Small delay to ensure actor loop has started
sleep(100);
send coordinator.start();
// Give actors time to process messages and callbacks
sleep(500);
reply(value)si può chiamare solo dentro un message handler di un actor e quando c'è un mittente a cui rispondere- Il runtime correla automaticamente la reply con la chiamata originale
send ... then (...) { ... } - Il callback gira nel contesto dell'actor mittente, con
selfche si riferisce al mittente
17.4 Message handler
I message handler sono funzioni che processano i messaggi in arrivo. Si dichiarano con la keyword on:
actor MyActor {
// Handler with no parameters
on handlerName() {
// Process message
}
// Handler with parameters
on handlerWithParams(param1, param2) {
// Process message with parameters
}
// Default handler (receives any unmatched message)
on handle(msg) {
// Process any message
}
}
Risoluzione degli handler
Quando viene inviato un messaggio, il runtime determina quale handler chiamare con le seguenti regole:
- Se viene fornito un nome di handler esplicito (tramite
send target.handlerName(...)), viene usato quel nome di handler - Altrimenti, se il payload del messaggio è una stringa, viene usato come nome dell'handler
- Altrimenti, viene usato l'handler
handlecome default - Il runtime cerca un handler con quel nome
- Se non viene trovato un handler specifico, l'handler
handleviene usato come fallback (se definito) - Se non viene trovato alcun handler, viene sollevato un errore di runtime
Esempi:
actor MyActor {
on handle(msg) {
print("Default handler: " + msg);
}
on process(data) {
print("Process handler: " + data);
}
}
var actor = spawn MyActor();
// Explicit handler name - calls process()
send actor.process(123);
// No handler name, non-string payload - calls handle()
send actor(456);
// No handler name, string payload - uses string as handler name, calls process()
send actor("process");
Parametri degli handler
- Il primo parametro riceve il payload del messaggio
- Si possono inviare parametri aggiuntivi passando più valori in sequenza
- Gli handler possono accedere ai campi dell'actor e chiamare altri handler
17.5 Isolamento dello stato degli actor
Ogni istanza di actor ha uno stato completamente isolato. I campi dichiarati in un actor sono privati di quella istanza:
actor BankAccount {
var balance = 0;
var accountName;
function BankAccount(name) {
accountName = name;
}
on deposit(amount) {
balance = balance + amount;
}
on withdraw(amount) {
if (balance >= amount) {
balance = balance - amount;
}
}
}
// Each instance has its own isolated state
var account1 = spawn BankAccount("Alice");
var account2 = spawn BankAccount("Bob");
send account1.deposit(100); // account1.balance = 100
send account2.deposit(200); // account2.balance = 200 (independent)
17.6 Riferimento self
Dentro i message handler di un actor, la keyword self fornisce un riferimento all'istanza corrente dell'actor:
actor PingPong {
var partner;
on setPartner(other) {
partner = other;
}
on ping() {
print("Received ping");
if (partner != null) {
send partner.pong(self); // Send self reference
}
}
on pong(sender) {
print("Received pong");
if (sender != null) {
send sender.ping();
}
}
}
var ping = spawn PingPong();
var pong = spawn PingPong();
send ping.setPartner(pong);
send pong.setPartner(ping);
Uso del riferimento self
selfsi può usare solo dentro i message handler di un actorselfrestituisce unActorReferenceall'actor corrente- Utile perché un actor possa inviare messaggi a se stesso o passare il proprio riferimento ad altri actor
17.7 Ricezione dei messaggi
Dentro un message handler di un actor, puoi usare la funzione receive() per attendere e recuperare il messaggio successivo dalla mailbox dell'actor. Per la maggior parte dei casi d'uso, però, è consigliato usare il messaging call-style con i callback:
actor Echo {
on echo(msg) {
var received = receive();
print($"Echo received: {received}");
}
}
var echo = spawn Echo();
send echo.echo("hello"); // Pass message as argument
Dettagli della funzione receive
receive()si può chiamare solo dentro un message handler di un actorreceive()si blocca finché un messaggio non arriva nella mailbox dell'actor- Per la maggior parte dei casi d'uso, preferisci il messaging call-style con callback usando
send target.handler(args) then (result) { ... }ereply()
Dichiarazioni di messaggio degli actor (Actor Sugar)
Per actor più strutturati, in stile protocollo, puoi dichiarare i messaggi in modo esplicito nel corpo dell'actor usando la keyword message e gestirli con un loop receive() + match dentro un handler:
actor Counter {
message Inc(amount);
message Get() -> int;
var value = 0;
on start() {
var running = true;
while (running) {
var msg = receive();
match msg {
case Inc(n): value = value + n;
case Get(): reply(value);
case ""stop"": running = false;
default: {};
}
}
}
}
var c = spawn Counter();
send c.start();
send c.Inc(1);
send c.Inc(2);
send c.Get() then (result) {
print($"Result: {result}"); // Prints: Result: 3
};
send c("stop");
message Inc(amount);emessage Get() -> int;dichiarano le forme dei messaggi che l'actor si aspetta.- I messaggi si possono inviare sia con
send actor.MessageName(args...)sia inviando un valore che codifica il messaggio (es. tramite un sum type) e facendo match su di esso doporeceive(). - Dentro il loop,
matchpuò usare pattern di variante (es.case Inc(n):) e pattern letterali (es.case ""stop"":). - Il built-in
reply(value)funziona anche negli handler basati sureceive(), così le dichiarazioni di messaggio con tipi di ritorno possono partecipare a protocolli request/response. - Le dichiarazioni di messaggio sono opzionali; documentano il protocollo dell'actor e si abbinano in modo naturale al pattern matching sui valori restituiti da
receive(). - Nessuno
sleep()necessario nel loop: Poichéreceive()si blocca finché non arriva un messaggio, il loopwhileattende automaticamente i messaggi senza consumare CPU. Il loop continua solo quando un messaggio viene ricevuto e processato.
17.8 Message passing tra actor
Gli actor possono inviarsi messaggi a vicenda, abilitando pattern di comunicazione complessi:
actor Sender {
on start(receiver) {
print("Sender: Starting...");
send receiver.hello();
send receiver.world();
}
}
actor Receiver {
var messageCount = 0;
on hello() {
messageCount = messageCount + 1;
print($"Receiver: Received 'hello' (total: {messageCount})");
}
on world() {
messageCount = messageCount + 1;
print($"Receiver: Received 'world' (total: {messageCount})");
}
}
var receiver = spawn Receiver();
var sender = spawn Sender();
send sender.start(receiver); // Pass receiver reference
17.9 Elaborazione concorrente
Gli actor processano i messaggi in concorrenza. Più actor possono girare contemporaneamente e ogni actor processa i propri messaggi in sequenza:
actor Worker {
var workerId;
var taskCount = 0;
function Worker(id) {
workerId = id;
}
on processTask(task) {
taskCount = taskCount + 1;
print($"Worker {workerId}: Processed task {task} (total: {taskCount})");
}
}
// Create multiple workers
var workers = [];
for (var i = 0; i < 5; i = i + 1) {
workers.append(spawn Worker(i + 1));
}
// Small delay to ensure actor loops have started
sleep(100);
// Send tasks to all workers concurrently
for (var i = 0; i < 10; i = i + 1) {
for (var j = 0; j < workers.length; j = j + 1) {
send workers[j].processTask(i);
}
}
// Give workers time to process all tasks
sleep(500);
17.10 Ciclo di vita degli actor
Creazione di un actor
- Gli actor si creano con
spawn ActorName(args) - Il costruttore viene chiamato durante l'inizializzazione
- L'actor inizia a processare i messaggi immediatamente
Esecuzione di un actor
- Ogni actor gira nel proprio task/thread
- Gli actor processano i messaggi dalla propria mailbox in sequenza
- I messaggi vengono accodati se l'actor è occupato a processarne uno
Terminazione di un actor
- Gli actor continuano a girare fino alla terminazione del programma
- Gli actor si possono fermare in modo programmatico con il metodo
stop() - Gli actor possono fermare se stessi chiamando
self.stop()in un message handler - Quando vengono fermati, gli actor finiscono di processare il messaggio corrente e poi escono
Fermare un actor dall'esterno
Per fermare un actor dall'esterno, chiama il metodo stop() sul riferimento all'actor:
var actor = spawn MyActor();
// ... use the actor ...
actor.stop(); // Stop the actor externally
Fermare un actor tramite messaggio
Gli actor possono anche fermare se stessi gestendo un messaggio di stop. È utile quando l'actor deve fare cleanup o prendere decisioni prima di fermarsi:
actor Worker {
var isRunning = true;
var workCount = 0;
on doWork() {
if (isRunning) {
workCount = workCount + 1;
print($"Worker: Completed work #{workCount}");
}
}
on stop() {
print("Worker: Received stop message, shutting down...");
// Perform any cleanup here
isRunning = false;
self.stop(); // Actor stops itself
}
}
var worker = spawn Worker();
send worker.doWork();
send worker.stop(); // Send stop message
Nota: Dopo aver chiamato stop() (dall'esterno o tramite self.stop()), l'actor finisce di processare il messaggio corrente (se ce n'è uno) e poi esce. Qualsiasi messaggio inviato all'actor dopo che è stato fermato andrà perso.
17.11 Riepilogo delle funzionalità degli actor
Funzionalità principali
- ✅ Stato isolato: Ogni istanza di actor ha il proprio stato isolato
- ✅ Elaborazione sequenziale: I messaggi vengono processati uno alla volta per actor
- ✅ Messaging asincrono: Invio dei messaggi non bloccante
- ✅ Esecuzione concorrente: Più actor girano in concorrenza
- ✅ Riferimento self: Keyword
selfper l'auto-riferimento dell'actor - ✅ Message handler: Keyword
onper definire i message handler - ✅ Handler di default: Handler
handleper i messaggi senza corrispondenza - ✅ Funzione receive:
receive()per attendere i messaggi dentro gli handler - ✅ Request/Response:
send ... then (result) { ... }conreply()per i callback - ✅ Gestione del ciclo di vita:
stop()eself.stop()per la terminazione dell'actor - ✅ Type safety: I riferimenti agli actor sono fortemente tipizzati
Parità tra interprete e modalità transpile
Le funzionalità degli actor sono supportate in modalità interprete, in modalità transpile C# e in modalità backend JavaScript (runtime locale):
- ✅ Dichiarazione, spawn e invio dei messaggi degli actor
- ✅ Invio dei messaggi call-style
send target.handler(args) - ✅ Send call-style con callback:
send target.handler(args) then (result) { ... } - ✅ Timeout dei messaggi:
send ... then (...) { ... } timeout ms catch (error) { ... } - ✅ Funzione
reply()per rispondere ai callback - ✅ Funzione
receive()per attendere i messaggi - ✅ Keyword
selfper l'auto-riferimento dell'actor - ✅
stop()eself.stop()per la gestione del ciclo di vita dell'actor - ✅ Gestione degli errori: Un uso non valido (es. usare
selffuori dagli handler) solleva errori coerenti in entrambe le modalità
Quando compili con --mode transpile, il codice degli actor viene transpile in C# e usa la stessa semantica di runtime dell'interprete. Quando compili con --mode js, il codice degli actor gira sul runtime actor JavaScript locale e preserva la stessa semantica di base per l'ordine della mailbox, i callback, i timeout, receive() e il comportamento di stop.
Limite attuale: gli actor JavaScript sono locali al processo del runtime JavaScript; la comunicazione actor seamless dal browser al server non è abilitata di default in questa fase.
Casi d'uso
- Elaborazione concorrente senza locking esplicito
- Servizi stateful con stato isolato
- Architetture a message-passing
- Pool di worker e distribuzione dei task
- Sistemi event-driven
- Simulazione di sistemi distribuiti
Esempio completo
actor Counter {
var count = 0;
on increment() {
count = count + 1;
print($"Count: {count}");
}
on decrement() {
count = count - 1;
print($"Count: {count}");
}
on reset() {
count = 0;
print("Counter reset");
}
on get() {
return count;
}
}
// Spawn and use
var counter = spawn Counter();
send counter.increment();
send counter.increment();
send counter.increment();
send counter.decrement();
send counter.reset();
// Give actor time to process messages (important in transpiled mode)
sleep(500);
Nota: Quando usi gli actor in modalità transpile (eseguibili compilati), è consigliato aggiungere un piccolo delay dopo lo spawn degli actor e alla fine dei programmi, per assicurarti che i messaggi vengano processati prima che il programma termini. Usa sleep(100) dopo lo spawn degli actor e sleep(500) alla fine dei programmi che usano gli actor.
17.12 Usare gli actor con gli agenti
Actor e agenti funzionano bene insieme. Gli agenti sono classi pensate per interazioni AI sincrone; memorizzare gli agenti nello stato di un actor abilita l'elaborazione concorrente e sessioni di agente isolate. È particolarmente utile per:
- Elaborazione concorrente di agenti: Fai spawn di più istanze di actor, ciascuna con il proprio agente, per processare i task in parallelo
- Isolamento dello stato: Ogni actor mantiene la propria istanza di agente e la propria cronologia di conversazione
- Sessioni di lunga durata: Gli actor possono mantenere lo stato dell'agente attraverso più interazioni a messaggi
- Coordinamento basato sui messaggi: Coordina più agenti tramite il message passing degli actor
Esempio:
actor AIAssistant {
var agent;
function AIAssistant(client) {
agent = new Agent("Assistant", "helper", "You help users.", client);
}
on ask(question) {
var response = agent.think(question);
print(response.content);
}
}
var client = new OpenRouterClient();
var assistant = spawn AIAssistant(client);
send assistant.ask("What is MALDA?");
Per informazioni dettagliate su come combinare agenti e actor, inclusi i pattern per l'elaborazione concorrente di agenti e l'orchestrazione multi-agente, vedi Usare gli agenti con gli actor nel capitolo Orchestrazione di agenti.