Coroutines in Lua
Le coroutine sono una delle caratteristiche piu’ potenti e distintive di Lua. Permettono di scrivere codice concorrente cooperativo senza la complessita’ dei thread tradizionali. Una coroutine puo’ sospendere la propria esecuzione con yield e riprenderla successivamente con resume, mantenendo intatto il proprio stato.
Cosa Sono le Coroutine
Una coroutine e’ simile a un thread, ma con una differenza fondamentale: le coroutine sono cooperative, non preemptive. Questo significa che una coroutine deve esplicitamente cedere il controllo tramite coroutine.yield(). Non esiste un sistema esterno che interrompe l’esecuzione.
| Caratteristica | Thread | Coroutine |
|---|---|---|
| Tipo di multitasking | Preemptive | Cooperativo |
| Cambio di contesto | Automatico (scheduler OS) | Esplicito (yield/resume) |
| Parallelismo reale | Si | No |
| Condivisione memoria | Necessita’ di lock | Nessun conflitto |
| Complessita’ | Alta | Bassa |
Creare e Avviare una Coroutine
La funzione coroutine.create() crea una nuova coroutine a partire da una funzione. La coroutine non viene eseguita immediatamente, ma resta in stato “suspended”.
local function saluta(nome)
print("Ciao, " .. nome .. "!")
coroutine.yield()
print("Bentornato, " .. nome .. "!")
coroutine.yield()
print("Arrivederci, " .. nome .. "!")
end
local co = coroutine.create(saluta)
print(coroutine.status(co)) -- "suspended"
coroutine.resume(co, "Marco") -- Stampa: "Ciao, Marco!"
print(coroutine.status(co)) -- "suspended"
coroutine.resume(co) -- Stampa: "Bentornato, Marco!"
print(coroutine.status(co)) -- "suspended"
coroutine.resume(co) -- Stampa: "Arrivederci, Marco!"
print(coroutine.status(co)) -- "dead"
Gli Stati di una Coroutine
Una coroutine puo’ trovarsi in quattro stati:
- suspended – creata ma non ancora avviata, oppure sospesa con yield
- running – attualmente in esecuzione
- normal – ha ripreso un’altra coroutine ed e’ in attesa
- dead – ha terminato l’esecuzione (o e’ uscita con un errore)
local function mostra_stato()
print("Dentro la coroutine, il mio stato e': " ..
coroutine.status(coroutine.running()))
coroutine.yield()
end
local co = coroutine.create(mostra_stato)
print("Prima del resume: " .. coroutine.status(co)) -- "suspended"
coroutine.resume(co) -- "running"
print("Dopo il yield: " .. coroutine.status(co)) -- "suspended"
coroutine.resume(co)
print("Dopo la fine: " .. coroutine.status(co)) -- "dead"
Scambio di Dati con yield e resume
Una delle caratteristiche piu’ utili e’ la capacita’ di passare dati tra la coroutine e il codice chiamante.
local function generatore_quadrati(n)
for i = 1, n do
coroutine.yield(i * i) -- restituisce il quadrato
end
end
local co = coroutine.create(generatore_quadrati)
for i = 1, 5 do
local ok, valore = coroutine.resume(co, 5)
if ok and valore then
print("Quadrato di " .. i .. " = " .. valore)
end
end
-- Quadrato di 1 = 1
-- Quadrato di 2 = 4
-- Quadrato di 3 = 9
-- Quadrato di 4 = 16
-- Quadrato di 5 = 25
Il primo argomento di resume (dopo la coroutine) viene passato come argomento della funzione al primo resume, e come valore di ritorno di yield nei resume successivi.
local function echo()
local messaggio = coroutine.yield() -- attende un messaggio
while messaggio do
print("Ricevuto: " .. messaggio)
messaggio = coroutine.yield("ok") -- restituisce conferma
end
end
local co = coroutine.create(echo)
coroutine.resume(co) -- avvia la coroutine
local _, risposta = coroutine.resume(co, "Ciao") -- "Ricevuto: Ciao"
print(risposta) -- "ok"
coroutine.resume(co, "Mondo") -- "Ricevuto: Mondo"
coroutine.resume(co, nil) -- termina il ciclo
coroutine.wrap
coroutine.wrap() crea una coroutine e restituisce una funzione che, ogni volta che viene chiamata, esegue un resume. E’ un’alternativa piu’ comoda quando si vuole usare la coroutine come un iteratore.
local function fibonacci(n)
local a, b = 0, 1
for i = 1, n do
coroutine.yield(a)
a, b = b, a + b
end
end
local fib = coroutine.wrap(function() fibonacci(10) end)
for numero in fib do
io.write(numero .. " ")
end
print()
-- 0 1 1 2 3 5 8 13 21 34
Confronto tra create e wrap
-- Con coroutine.create (piu' controllo)
local co = coroutine.create(function()
coroutine.yield(1)
coroutine.yield(2)
coroutine.yield(3)
end)
local ok, val = coroutine.resume(co) -- ok=true, val=1
print(ok, val)
-- Con coroutine.wrap (piu' semplice)
local next_val = coroutine.wrap(function()
coroutine.yield(1)
coroutine.yield(2)
coroutine.yield(3)
end)
print(next_val()) -- 1
print(next_val()) -- 2
print(next_val()) -- 3
Pattern Producer-Consumer
Le coroutine sono ideali per implementare il pattern producer-consumer senza buffer condivisi o lock.
local function produttore(elementi)
return coroutine.wrap(function()
for _, elemento in ipairs(elementi) do
print(" Produttore: invio " .. elemento)
coroutine.yield(elemento)
end
end)
end
local function consumatore(fonte)
local totale = 0
for elemento in fonte do
print(" Consumatore: ricevuto " .. elemento)
totale = totale + elemento
end
return totale
end
local dati = {10, 20, 30, 40, 50}
local prodotto = produttore(dati)
local risultato = consumatore(prodotto)
print("Totale elaborato: " .. risultato) -- 150
Esempio Pratico: Task Scheduler
Un semplice scheduler cooperativo che esegue piu’ task in modo alternato.
local Scheduler = {}
function Scheduler.nuovo()
local self = {coda = {}}
function self.aggiungi_task(nome, funzione)
local co = coroutine.create(funzione)
table.insert(self.coda, {nome = nome, coroutine = co})
end
function self.esegui()
while #self.coda > 0 do
local prossimi = {}
for _, task in ipairs(self.coda) do
local ok, err = coroutine.resume(task.coroutine)
if not ok then
print("[ERRORE] Task '" .. task.nome .. "': " .. tostring(err))
end
if coroutine.status(task.coroutine) ~= "dead" then
table.insert(prossimi, task)
else
print("[COMPLETATO] Task '" .. task.nome .. "'")
end
end
self.coda = prossimi
end
print("Tutti i task completati.")
end
return self
end
-- Utilizzo dello scheduler
local s = Scheduler.nuovo()
s.aggiungi_task("Download", function()
for i = 1, 3 do
print(" Download: progresso " .. (i * 33) .. "%")
coroutine.yield()
end
end)
s.aggiungi_task("Elaborazione", function()
for i = 1, 4 do
print(" Elaborazione: step " .. i)
coroutine.yield()
end
end)
s.aggiungi_task("Salvataggio", function()
for i = 1, 2 do
print(" Salvataggio: blocco " .. i)
coroutine.yield()
end
end)
s.esegui()
Esempio Pratico: Pipeline di Trasformazione
Le coroutine possono essere concatenate per creare pipeline di elaborazione dati.
local function genera_numeri(n)
return coroutine.wrap(function()
for i = 1, n do
coroutine.yield(i)
end
end)
end
local function filtra_pari(fonte)
return coroutine.wrap(function()
for valore in fonte do
if valore % 2 == 0 then
coroutine.yield(valore)
end
end
end)
end
local function moltiplica(fonte, fattore)
return coroutine.wrap(function()
for valore in fonte do
coroutine.yield(valore * fattore)
end
end)
end
-- Pipeline: genera 1-20 -> filtra pari -> moltiplica per 10
local pipeline = moltiplica(filtra_pari(genera_numeri(20)), 10)
local risultati = {}
for valore in pipeline do
table.insert(risultati, valore)
end
print(table.concat(risultati, ", "))
-- 20, 40, 60, 80, 100, 120, 140, 160, 180, 200
Gestione degli Errori nelle Coroutine
Se una coroutine genera un errore, coroutine.resume() restituisce false seguito dal messaggio di errore, senza propagare l’eccezione al codice chiamante.
local co = coroutine.create(function()
print("Inizio operazione...")
coroutine.yield()
error("Qualcosa e' andato storto!")
end)
local ok, err = coroutine.resume(co) -- ok=true
print("Prima ripresa: " .. tostring(ok))
local ok2, err2 = coroutine.resume(co) -- ok=false
print("Seconda ripresa: " .. tostring(ok2) .. " - " .. tostring(err2))
print("Stato: " .. coroutine.status(co)) -- "dead"
Conclusione
Le coroutine di Lua offrono un modello di concorrenza elegante e leggero. Sono perfette per implementare iteratori personalizzati, pipeline di dati, scheduler cooperativi e pattern producer-consumer. A differenza dei thread, non introducono problemi di race condition o necessita’ di sincronizzazione. L’API compatta (create, resume, yield, wrap, status) le rende semplici da apprendere ma estremamente versatili nella pratica.