Lua Coroutine
* * *
## What is a Coroutine?
Lua coroutines are similar to threads: they have their own independent stacks, local variables, and instruction pointers, while 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 can also be complex to use.
## Basic Syntax
Coroutines are supported by the `coroutine` module.
To use coroutines, you can create a new coroutine object within a function using **`coroutine.create`** and start its execution using **`coroutine.resume`**. A coroutine can actively pause its own execution by calling **`coroutine.yield`** and return control to the caller.
| Method | Description |
| --- | --- |
| `coroutine.create()` | Creates a coroutine, returns the coroutine. The argument is a function. When used with `resume`, it wakes up the function call. |
| `coroutine.resume()` | Restarts a coroutine. Used in conjunction with `create`. |
| `coroutine.yield()` | Suspends a coroutine, setting it to a suspended state. When used with `resume`, it can have many useful effects. |
| `coroutine.status()` | Checks the status of a coroutine. Note: A coroutine has three states: dead, suspended, running. Refer to the program below for details on when these states occur. |
| `coroutine.wrap()` | Creates a coroutine, returns a function. Once you call this function, you enter the coroutine. Its functionality overlaps with `create`. |
| `coroutine.running()` | Returns the currently 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
```lua
function foo()
print("Coroutine foo starts executing")
local value = coroutine.yield("Suspend foo's execution")
print("Coroutine foo resumes execution, passed value: " .. tostring(value))
print("Coroutine foo finishes executing")
end
-- Create a coroutine
local co = coroutine.create(foo)
-- Start the coroutine
local status, result = coroutine.resume(co)
print(result) -- Output: Suspend foo's execution
-- Resume the coroutine's execution, passing a value
status, result = coroutine.resume(co, 42)
print(result) -- Output: Coroutine foo resumes execution, passed value: 42
In the example above, we defined a function named `foo` as a 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 executes until it reaches `coroutine.yield`, pauses, and returns the value to the main program. Then, we call `coroutine.resume` again, passing a value as an argument for when the coroutine resumes execution.
Executing the above code produces the following output:
Coroutine foo starts executing
Suspend foo's execution
Coroutine foo resumes execution, passed value: 42
Coroutine foo finishes executing
nil
It's important to note that the state of a coroutine can be obtained using the `coroutine.status` function. By checking the state, you can determine the execution status of the coroutine (e.g., running, suspended, dead, etc.).
### The following example demonstrates the usage of each method:
## coroutine_test.lua file
```lua
-- coroutine_test.lua file
-- Creates a new coroutine object `co`, where the coroutine function prints the passed argument `i`
co = coroutine.create(
function(i)
print(i);
end
)
-- Uses `coroutine.resume` to start the execution of coroutine `co`, passing argument 1. The coroutine starts executing, printing output 1.
coroutine.resume(co, 1) -- 1
-- Checks the status of coroutine `co` using `coroutine.status`, output is 'dead', indicating the coroutine has finished executing.
print(coroutine.status(co)) -- dead
print("----------")
-- Uses `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("----------")
-- Creates another coroutine object `co2`, where the coroutine function prints numbers 1 to 10 in a loop. When the loop reaches 3, it prints the current coroutine's status 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
)
-- Continuously calls `coroutine.resume` to start the execution of coroutine `co2`.
coroutine.resume(co2) -- 1
coroutine.resume(co2) -- 2
coroutine.resume(co2) -- 3
-- Checks the status of coroutine `co2` using `coroutine.status`, output is 'suspended', indicating the coroutine is paused.
print(coroutine.status(co2)) -- suspended
print(coroutine.running())
print("----------")
The output of executing the above example is:
1
dead
----------
1
----------
1
2
3
running
thread: 0x7fb801c05868
false
suspended
thread: 0x7fb801c04c88
true
----------
From `coroutine.running`, you can see that a coroutine is essentially a thread at the underlying implementation level.
When you create a coroutine, you are registering an event in a new thread.
When you use `resume` to trigger the event, the coroutine function created is executed. When it encounters `yield`, it means the current thread is suspended, waiting for another `resume` to trigger the event.
Next, let's analyze a more detailed example:
## Example
```lua
function foo(a)
print("foo function outputs", a)
return coroutine.yield(2 * a) -- Returns 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) -- a, b are the values passed during the first coroutine call
print("Third coroutine execution output", r, s)
return b, "End coroutine" -- b is the value passed during the second coroutine call
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 output of executing the above example is:
First coroutine execution output 1 10
foo function outputs 2
main true 4
--separator----
Second coroutine execution output r
main true 11 -9
---separator---
Third coroutine execution output x y
main true 10 end
---separator---
main false cannot resume dead coroutine
---separator---
The above example is explained as follows:
* Calling `resume` wakes up the coroutine. If the `resume` operation is successful, it returns `true`; otherwise, it returns `false`.
* The coroutine runs.
* It runs to the `yield` statement.
* `yield` suspends the coroutine, and the first `resume` returns. (Note: Here, `yield` returns, and its parameters are the arguments of `resume`).
* The second `resume` wakes up the coroutine again. (Note: Here, the parameters of `resume`, except the first one, become the arguments for `yield`).
* `yield` returns.
* The coroutine continues to run.
* If you continue to call the `resume` method after the used coroutine has finished running, it will output: `cannot resume dead coroutine`.
The power of the cooperation between `resume` and `yield` lies in that `resume` is in the main thread, passing external state (data) into the coroutine; while `yield` returns internal state (data) back to the main thread.
* * *
## Producer-Consumer Problem
Now I will use Lua coroutines to solve the classic producer-consumer problem.
## Example
```lua
local newProductor
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, the coroutine is suspended.
end
-- Start the program
newProductor = coroutine.create(productor)
consumer()
The output of executing the above example is:
1
2
3
4
5
6
7
8
9
10
11
12
13
β¦β¦
* * *
## Differences Between Threads and Coroutines
The main difference between threads and coroutines is that a program with multiple threads can run several threads simultaneously, while coroutines need to run in a cooperative manner.
At any given moment, only one coroutine is running, and this running coroutine is only suspended when explicitly requested to do so.
Coroutines are somewhat similar to synchronous multithreading, where several threads waiting for the same lock resemble coroutines.
The main differences are summarized as follows:
* **Scheduling Method:** Threads are typically preemptively scheduled by the operating system's scheduler, which switches execution rights between different coroutines. Coroutines, on the other hand, are cooperatively scheduled, and the transfer of execution rights is explicitly controlled by the programmer.
* **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 in a running state at a time, and other coroutines must wait for the current coroutine to voluntarily give up execution rights.
* **Memory Usage:** Threads usually require independent 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 to be considered. Coroutines typically share data through parameter passing and return values, providing better data isolation between different coroutines.
* **Debugging and Error Handling:** Threads are generally more complex to debug and handle errors because the interaction and concurrent execution between multiple threads can lead to difficult-to-debug issues. Coroutines are relatively simpler to debug and handle errors because their execution flow is explicitly controlled by the programmer.
Overall, threads are suitable for scenarios requiring concurrent execution, such as leveraging parallelism on multi-core processors to speed up task execution. Coroutines are suitable for scenarios requiring cooperation and coordination, such as state machines, event-driven programming, or cooperative task processing. The choice between using threads or coroutines depends on the specific application requirements and programming model.
YouTip