Lua corrutinas (coroutine)
¿Qué es una corrutina (coroutine)?
Las corrutinas (coroutine) de Lua son similares a los hilos: tienen una pila independiente, variables locales independientes, un puntero de instrucción independiente, y al mismo tiempo comparten variables globales y la mayoría de las demás cosas con otras corrutinas.
Las corrutinas pueden entenderse como un tipo especial de hilo que puede pausar y reanudar su ejecución, permitiendo así la multitarea no preventiva.
Las corrutinas son una característica muy potente, pero también son complejas de usar.
Sintaxis básica
Las corrutinas están respaldadas por elcoroutinemódulo.
Al usar corrutinas, puedes usar en una funcióncoroutine.createcrear un nuevo objeto de corrutina y usarcoroutine.resumeiniciar su ejecución. Una corrutina puede, al llamar acoroutine.yieldsuspender activamente su propia ejecución y devolver el control al llamador.
| Método | Descripción |
|---|---|
| coroutine.create() | Crea una coroutine, devuelve una coroutine; el parámetro es una función. Cuando se usa junto con resume, se activa la llamada a la función. |
| coroutine.resume() | Reanuda la coroutine, se usa junto con create. |
| coroutine.yield() | Suspende la coroutine, poniéndola en estado suspendido; combinado con resume puede tener muchos efectos útiles. |
| coroutine.status() | Consulta el estado de la coroutine. Nota: la coroutine tiene tres estados: dead, suspended, running. Consulta el siguiente programa para saber cuándo se tiene cada estado. |
| coroutine.wrap() | Crea una coroutine y devuelve una función; en cuanto llamas a esa función, entras en la coroutine. Su función es equivalente a create. |
| coroutine.running() | Devuelve la coroutine en ejecución. Una coroutine es un hilo; cuando se usa running, devuelve el número de hilo de una coroutine. |
El siguiente ejemplo demuestra cómo usar las corrutinas de Lua:
Ejemplo
print("La corrutina foo comienza la ejecución")
local value = coroutine.yield("Se pausa la ejecución de foo")
print("La corrutina foo reanuda la ejecución, el valor pasado es: " .. tostring(value))
print("La corrutina foo finaliza la ejecución")
end
-- Crear la corrutina
local co = coroutine.create(foo)
-- Iniciar la corrutina
local status, result = coroutine.resume(co)
print(result) -- Salida: Se pausa la ejecución de foo
-- Reanudar la ejecución de la corrutina y pasar un valor
status, result = coroutine.resume(co, 42)
print(result) -- Salida: La corrutina foo reanuda la ejecución, el valor pasado es: 42
En el ejemplo anterior, definimos una función llamada foo como corrutina. Dentro de la función, usamos coroutine.yield para pausar la ejecución de la corrutina y devolvimos un valor.
. En el programa principal, usamos coroutine.create para crear un objeto de corrutina y coroutine.resume para iniciar su ejecución.
Después de la primera llamada a coroutine.resume, la corrutina se ejecuta hasta coroutine.yield, se pausa y devuelve el valor al programa principal. Luego, llamamos de nuevo a coroutine.resume y pasamos un valor como parámetro cuando la corrutina reanuda su ejecución.
El resultado de ejecutar el código anterior es:
协同程序 foo 开始执行 暂停 foo 的执行 协同程序 foo 恢复执行,传入的值为: 42 协同程序 foo 结束执行 nil
Es importante tener en cuenta que el estado de la corrutina se puede obtener mediante la función coroutine.status. Al verificar el estado se puede determinar la situación de ejecución de la corrutina (por ejemplo, en ejecución, suspendida, finalizada, etc.).
El siguiente ejemplo demuestra el uso de los métodos anteriores:
Archivo coroutine_test.lua
-- Se crea un nuevo objeto de corrutina co, donde la función corrutina imprime el parámetro i pasado
co = coroutine.create(
function(i)
print(i);
end
)
-- Se usa coroutine.resume para iniciar la ejecución de la corrutina co, pasando el parámetro 1. La corrutina comienza a ejecutarse e imprime como salida 1
coroutine.resume(co, 1) -- 1
-- Se verifica el estado de la corrutina co con coroutine.status; la salida es dead, lo que indica que la corrutina ya ha terminado su ejecución
print(coroutine.status(co)) -- dead
print("----------")
-- Se usa coroutine.wrap para crear un envoltorio de corrutina, convirtiendo la función corrutina en un objeto función invocable directamente
co = coroutine.wrap(
function(i)
print(i);
end
)
co(1)
print("----------")
-- Se crea otro objeto de corrutina co2, donde la función corrutina imprime los números del 1 al 10 mediante un bucle; cuando el bucle llega a 3, muestra el estado actual de la corrutina y el hilo en ejecución
co2 = coroutine.create(
function()
for i=1,10 do
print(i)
if i == 3 then
print(coroutine.status(co2)) --running
print(coroutine.running()) --thread:XXXXXX
end
coroutine.yield()
end
end
)
-- Se llama repetidamente a coroutine.resume para iniciar la ejecución de la corrutina co2
coroutine.resume(co2) --1
coroutine.resume(co2) --2
coroutine.resume(co2) --3
-- Se verifica el estado de la corrutina co2 con coroutine.status; la salida es suspended, lo que indica que la corrutina está suspendida
print(coroutine.status(co2)) -- suspended
print(coroutine.running())
print("----------")
El resultado de ejecutar el ejemplo anterior es:
1 dead ---------- 1 ---------- 1 2 3 running thread: 0x7fb801c05868 false suspended thread: 0x7fb801c04c88 true ----------
Se puede ver con coroutine.running que, en la implementación subyacente, coroutine es un hilo.
Cuando se crea una coroutine con create, se registra un evento en un nuevo hilo.
Cuando se usa resume para disparar el evento, la función coroutine creada se ejecuta. Cuando se encuentra yield, significa que el hilo actual se suspende, esperando que otro resume dispare el evento.
A continuación analizamos un ejemplo más detallado:
Ejemplo
print("Salida de la función foo", a)
return coroutine.yield(2 * a) -- Devuelve el valor de 2*a
end
co = coroutine.create(function (a , b)
print("Salida de la primera ejecución de la corrutina", a, b) -- co-body 1 10
local r = foo(a + 1)
print("Salida de la segunda ejecución de la corrutina", r)
local r, s = coroutine.yield(a + b, a - b) -- Los valores de a y b son los pasados en la primera llamada a la corrutina
print("Salida de la tercera ejecución de la corrutina", r, s)
return b, "Fin de la corrutina" -- El valor de b es el pasado en la segunda llamada a la corrutina
end)
print("main", coroutine.resume(co, 1, 10)) -- true, 4
print("--línea divisoria----")
print("main", coroutine.resume(co, "r")) -- true 11 -9
print("---línea divisoria---")
print("main", coroutine.resume(co, "x", "y")) -- true 10 end
print("---línea divisoria---")
print("main", coroutine.resume(co, "x", "y")) -- cannot resume dead coroutine
print("---línea divisoria---")
El resultado de ejecutar el ejemplo anterior es:
第一次协同程序执行输出 1 10 foo 函数输出 2 main true 4 --分割线---- 第二次协同程序执行输出 r main true 11 -9 ---分割线--- 第三次协同程序执行输出 x y main true 10 结束协同程序 ---分割线--- main false cannot resume dead coroutine ---分割线---
El ejemplo anterior se explica de la siguiente manera:
- Se llama a resume para despertar la corrutina; si la operación resume tiene éxito, devuelve true; de lo contrario, devuelve false.
- La corrutina se ejecuta.
- Se ejecuta hasta la sentencia yield.
- yield suspende la corrutina, y el primer resume retorna. (Nota: aquí yield retorna, y el parámetro es el parámetro de resume).
- El segundo resume vuelve a despertar la corrutina. (Nota: entre los parámetros de este resume, excepto el primero, los restantes se pasan como parámetros de yield).
- yield retorna.
- La corrutina continúa ejecutándose.
- Si después de que la corrutina finaliza su ejecución se sigue llamando a resume, se obtiene el mensaje: cannot resume dead coroutine
La gran ventaja de la combinación de resume y yield es que resume está en el programa principal y transmite el estado (datos) externo al interior de la corrutina; mientras que yield devuelve el estado (datos) interno al programa principal.
Problema del productor-consumidor
Ahora usaremos las corrutinas de Lua para resolver el clásico problema del productor-consumidor.
Ejemplo
function productor()
local i = 0
while true do
i = i + 1
send(i) -- Enviar el artículo producido al consumidor
end
end
function consumer()
while true do
local i = receive() -- Obtener el artículo del productor
print(i)
end
end
function receive()
local status, value = coroutine.resume(newProductor)
return value
end
function send(x)
coroutine.yield(x) -- x representa el valor que se debe enviar; después de que el valor se devuelve, se suspende la corrutina
end
-- Iniciar el programa
newProductor = coroutine.create(productor)
consumer()
El resultado de ejecutar el ejemplo anterior es:
1 2 3 4 5 6 7 8 9 10 11 12 13 ……
Diferencia entre hilos y corrutinas
La principal diferencia entre hilos y corrutinas es que un programa con varios hilos puede ejecutar varios hilos al mismo tiempo, mientras que las corrutinas deben ejecutarse cooperando entre sí.
En un momento dado, solo hay una corrutina en ejecución, y esta corrutina en ejecución solo se suspende cuando se le pide explícitamente que se suspenda.
Las corrutinas son algo similares a la multitarea síncrona; varios hilos que esperan el mismo bloqueo de hilo son algo similares a las corrutinas.
Las principales diferencias se resumen a continuación:
Método de planificación: los hilos suelen ser planificados de forma preventiva por el planificador del sistema operativo, que alterna el derecho de ejecución entre diferentes hilos. En cambio, las corrutinas se planifican de forma no preventiva, y los programadores controlan explícitamente la transferencia del derecho de ejecución.
Concurrencia: los subprocesos (threads) se ejecutan de forma concurrente; varios subprocesos pueden ejecutarse simultáneamente en múltiples núcleos de procesador, o alternar su ejecución en un solo núcleo mediante el reparto de tiempo. Las corrutinas son cooperativas: solo una corrutina está en estado de ejecución, y las demás deben esperar a que la corrutina actual ceda voluntariamente el control de ejecución.
Ocupación de memoria: los subprocesos generalmente necesitan una pila y un contexto independientes, por lo que su creación y destrucción conlleva una sobrecarga adicional. Las corrutinas pueden compartir la misma pila y contexto, por lo que el costo de crearlas y destruirlas es menor.
Compartición de datos: los subprocesos pueden compartir espacio de memoria, pero hay que prestar atención a la seguridad de los hilos y a los problemas de sincronización. Las corrutinas suelen compartir datos mediante parámetros y valores de retorno, y el aislamiento de datos entre diferentes corrutinas es mejor.
Depuración y manejo de errores: los subprocesos suelen ser más complejos en depuración y manejo de errores, porque la interacción entre múltiples subprocesos y la ejecución concurrente pueden provocar problemas difíciles de depurar. Las corrutinas son relativamente más simples en depuración y manejo de errores, ya que el programador controla explícitamente el flujo de ejecución.
En general, los subprocesos son adecuados para escenarios que requieren ejecución concurrente, por ejemplo, aprovechar el paralelismo en procesadores multinúcleo para acelerar la ejecución de tareas. Las corrutinas son adecuadas para escenarios que requieren cooperación y coordinación, como máquinas de estados, programación orientada a eventos o procesamiento cooperativo de tareas. Elegir entre subprocesos o corrutinas depende de las necesidades específicas de la aplicación y del modelo de programación.
Otras extensiones