00
:
00
:
00
:
00
•Corso SEO AI - Usa SEOEMAIL al checkout per il 30% di sconto

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.