Lua Coroutine
What is a coroutine?
Lua coroutines are quite similar to threads: they have independent stacks, independent local variables, and independent instruction pointers, while also sharing global variables and most other things with other coroutines.
A coroutine can be understood as a special kind of thread that can pause and resume its execution, thereby allowing non-preemptive multitasking.
Coroutines are a very powerful feature, but they are also complex to use.
Basic Syntax
Coroutines arecoroutinesupported by the module.
When using coroutines, you can use thecoroutine.createCreate a new coroutine object and usecoroutine.resumeto start its execution. A coroutine cancoroutine.yieldvoluntarily pause its own execution and return control to the caller.
| Method | Description |
|---|---|
| coroutine.create() | Creates a coroutine, returns a coroutine, the parameter is a function. When used with resume, it wakes up the function call. |
| coroutine.resume() | Restarts a coroutine, used with create. |
| coroutine.yield() | Suspends a coroutine, sets the coroutine to suspended state. Used with resume, this can have many useful effects. |
| coroutine.status() | Checks the status of a coroutine. Note: coroutine has three states: dead, suspended, running. See the program below for when these states occur. |
| coroutine.wrap() | Creates a coroutine, returns a function. Once you call this function, you enter the coroutine. This duplicates the functionality of create. |
| coroutine.running() | Returns the running coroutine. A coroutine is a thread. When using running, it returns the thread ID of a coroutine. |
The following example demonstrates how to use Lua coroutines:
Example
print("Coroutine foo started executing")
local value = coroutine.yield("Pausing foo's execution")
print("Coroutine foo resumed execution, the value passed is: " .. tostring(value))
print("Coroutine foo finished execution")
end
-- Create a coroutine
local co = coroutine.create(foo)
-- Start the coroutine
local status, result = coroutine.resume(co)
print(result) -- Output: Pausing foo's execution
-- Resume the coroutine's execution and pass in a value
status, result = coroutine.resume(co, 42)
print(result) -- Output: Coroutine foo resumed execution, the value passed is: 42
In the above example, we defined a function named foo as the coroutine. Inside the function, we used coroutine.yield to pause the coroutine's execution and return a value.
In the main program, we used coroutine.create to create a coroutine object, and coroutine.resume to start its execution.
After the first call to coroutine.resume, the coroutine pauses at coroutine.yield and returns the value to the main program. Then, we call coroutine.resume again, passing in a value as the parameter for the coroutine to resume execution.
Executing the above code produces the following output:
协同程序 foo 开始执行 暂停 foo 的执行 协同程序 foo 恢复执行,传入的值为: 42 协同程序 foo 结束执行 nil
It should be noted that the state of a coroutine can be obtained through the coroutine.status function. By checking the state, you can determine the execution status of the coroutine (such as running, suspended, ended, etc.).
The following example demonstrates the usage of the above methods:
coroutine_test.lua file
-- Create a new coroutine object co, where the coroutine function prints the passed parameter i
co = coroutine.create(
function(i)
print(i);
end
)
-- Use coroutine.resume to start the execution of coroutine co, passing in parameter 1. The coroutine starts executing and prints output 1.
coroutine.resume(co, 1) -- 1
-- Check the state of coroutine co via coroutine.status, the output is dead, indicating the coroutine has finished execution.
print(coroutine.status(co)) -- dead
print("----------")
-- Use coroutine.wrap to create a coroutine wrapper, converting the coroutine function into a directly callable function object.
co = coroutine.wrap(
function(i)
print(i);
end
)
co(1)
print("----------")
-- Create another coroutine object co2, where the coroutine function prints numbers 1 to 10 in a loop, and when the loop reaches 3, it outputs the current coroutine state and the running thread.
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
)
-- Call coroutine.resume multiple times to start the execution of coroutine co2.
coroutine.resume(co2) --1
coroutine.resume(co2) --2
coroutine.resume(co2) --3
-- Check the state of coroutine co2 via coroutine.status, the output is suspended, indicating the coroutine is paused.
print(coroutine.status(co2)) -- suspended
print(coroutine.running())
print("----------")
The above example produces the following output:
1 dead ---------- 1 ---------- 1 2 3 running thread: 0x7fb801c05868 false suspended thread: 0x7fb801c04c88 true ----------
As can be seen from coroutine.running, a coroutine is implemented as a thread at the underlying level.
When you create a coroutine, it registers an event in a new thread.
When resume is used to trigger the event, the coroutine function created by create is executed. When yield is encountered, it means suspending the current thread and waiting for another resume to trigger the event.
Next, let's analyze a more detailed example:
Example
print("foo function output", a)
return coroutine.yield(2 * a) -- Return the value of 2*a
end
co = coroutine.create(function (a , b)
print("First coroutine execution output", a, b) -- co-body 1 10
local r = foo(a + 1)
print("Second coroutine execution output", r)
local r, s = coroutine.yield(a + b, a - b) -- The values of a and b are passed in when the coroutine is called for the first time
print("Third coroutine execution output", r, s)
return b, "End of coroutine" -- The value of b is passed in when the coroutine is called for the second time
end)
print("main", coroutine.resume(co, 1, 10)) -- true, 4
print("--Separator----")
print("main", coroutine.resume(co, "r")) -- true 11 -9
print("---Separator---")
print("main", coroutine.resume(co, "x", "y")) -- true 10 end
print("---Separator---")
print("main", coroutine.resume(co, "x", "y")) -- cannot resume dead coroutine
print("---Separator---")
The above example produces the following output:
第一次协同程序执行输出 1 10 foo 函数输出 2 main true 4 --分割线---- 第二次协同程序执行输出 r main true 11 -9 ---分割线--- 第三次协同程序执行输出 x y main true 10 结束协同程序 ---分割线--- main false cannot resume dead coroutine ---分割线---
The above example proceeds as follows:
- Call resume to wake up the coroutine. The resume operation returns true on success, otherwise it returns false.
- The coroutine runs.
- Runs to the yield statement.
- yield suspends the coroutine, and the first resume returns. (Note: here yield returns, and the parameters are the arguments of resume).
- The second resume wakes up the coroutine again. (Note: among the arguments of resume here, except for the first argument, the remaining arguments will serve as the parameters of yield).
- yield returns.
- The coroutine continues running.
- If, after the coroutine has finished running, you continue to call the resume method, it will output: cannot resume dead coroutine.
The power of combining resume and yield is that resume is in the main program and passes external state (data) into the coroutine, while yield returns internal state (data) back to the main program.
Producer-Consumer Problem
Now I will use Lua coroutines to solve the classic producer-consumer problem.
Example
function productor()
local i = 0
while true do
i = i + 1
send(i) -- Send the produced item to the consumer
end
end
function consumer()
while true do
local i = receive() -- Get an item from the producer
print(i)
end
end
function receive()
local status, value = coroutine.resume(newProductor)
return value
end
function send(x)
coroutine.yield(x) -- x represents the value to be sent. After the value is returned, suspend the coroutine.
end
-- Start the program
newProductor = coroutine.create(productor)
consumer()
The above example produces the following output:
1 2 3 4 5 6 7 8 9 10 11 12 13 ……
Difference Between Threads and Coroutines
The main difference between threads and coroutines is that a program with multiple threads can run several threads at the same time, whereas coroutines need to run in cooperation with each other.
Only one coroutine runs at any given moment, and the running coroutine is suspended only when it is explicitly asked to suspend.
Coroutines are somewhat like synchronous multithreading. Several threads waiting for the same thread lock are somewhat similar to coroutines.
The main differences are summarized as follows:
Scheduling method: Threads are usually preemptively scheduled by the operating system's scheduler, which switches execution rights between different threads. Coroutines, on the other hand, are non-preemptively scheduled; programmers explicitly control the transfer of execution rights.
Concurrency: Threads execute concurrently; multiple threads can run simultaneously on multiple processor cores, or switch execution on a single core via time-slicing. Coroutines are cooperative: only one coroutine is running at a time, and other coroutines must wait until the currently running coroutine voluntarily yields execution.
Memory usage: Threads usually require separate stacks and context environments, so creating and destroying threads incurs additional overhead. Coroutines can share the same stack and context, so the overhead of creating and destroying coroutines is smaller.
Data sharing: Threads can share memory space, but thread safety and synchronization issues need attention. Coroutines usually share data through parameter passing and return values, and data isolation between different coroutines is better.
Debugging and error handling: Threads are usually more complex in debugging and error handling, because interactions and concurrent execution among multiple threads can lead to problems that are hard to debug. Coroutines are relatively simpler in debugging and error handling, because programmers explicitly control the execution flow.
Overall, threads are suitable for scenarios requiring concurrent execution, such as using parallelism on multi-core processors to speed up task execution. Coroutines are suitable for scenarios requiring collaboration and coordination, such as state machines, event-driven programming, or cooperative task processing. Choosing whether to use threads or coroutines depends on specific application requirements and the programming model.
Other extensions