Advanced Lua Topics - Coroutines
Tutorial Index
Introduction: What are Coroutines?
The Basics: Creating a Coroutine
Resuming Coroutines - Let’s run some chunks
Getting the Status of our Coroutines
Yielding - Suspend our Coroutine
Because coroutines are one of the most advanced topics in Lua and requires a considerable working knowledge of the language in order to learn and understand them, to help you with some of the more complex concepts I have created a basic “dictionary” of terms which you can find at the end of this document. Important terms that I define there will be highlighted in red throughout the tutorial, while important functions will be highlighted in blue.
Up til now, you’ve probably been going about programming with a sequential mindset - functions are meant to be run one after the other, and multi-tasking is an abstract concept. Well no longer. Coroutines are Lua’s method of multi-tasking, and will allow us to do some really neat and complex things with relative ease once you understand the basics. Want to receive rednet messages while at the same time getting user input? Coroutines are for you. Want to monitor your reactor and at the same time have a fancy GUI to interact with it? Yup. Coroutines again.
Although coroutines can be somewhat complex and are one of the most advanced topics that Lua has to offer, once you understand them they will be a powerful tool in the hands of a good programmer.
Now what are coroutines exactly? Well in simple terms, coroutines are a way of executing blocks of code until a yielding point is encountered, at which point the function will stop running and allow the calling ancestor to run other code. Now you may think that this is not true multi-tasking - and you would be right to an extent. To be completely honest, Lua is a completely sequential language. But with coroutines, we will be executing and switching back and forth between blocks fast enough that you won’t even be able to tell a difference between true parallelism and coroutines.
Now that you understand the basic concept of a coroutine, let’s go over how we create one. Fortunately, making a coroutine is actually the simplest part about the subject.
To start, let’s create a basic function.
function myFirstCoroutine ( )
print(“Hello! I am a function.”)
end
Excellent. We have a basic function which will print out some text when it is called. Now let’s turn that function into a coroutine.
local theFirstCoroutine = coroutine.create ( myFirstCoroutine )
That’s it! Our variable, theFirstCoroutine, now refers to a coroutine which contains the block of code we defined in the myFirstCoroutine function.
If you run this code in a Lua environment, you will see that nothing happens. Let’s take a look now at how we can actually run the block.
Now that we’ve created a coroutine, let’s actually go ahead and run the function which it contains. To do so, we can use coroutine.resume.
function myFirstCoroutine ( )
print(“Hello! I am a function.”)
end
local theFirstCoroutine = coroutine.create ( myFirstCoroutine )
coroutine.resume(theFirstCoroutine)
Output: Hello! I am a function.
By calling coroutine.resume with our coroutine as the first argument, we’ve executed the code found inside myFirstCoroutine. To understand a bit better what’s going on under the hood here, allow me to introduce to you a great diagnostic tool - coroutine.status.
Let’s recreate the code we used in our first resume, but this time let’s add in a new function - coroutine.status. With this tool, we’ll be able to tell just how coroutines are running the function.
function SimpleCoroutine ( )
print(“Hello again. I’m a new coroutine.”)
end
local simpleCo = coroutine.create ( SimpleCoroutine )
print(“Status after creation: “..coroutine.status(simpleCo))
coroutine.resume(simpleCo)
print(“Status after resuming: “..coroutine.status(simpleCo))
Output:
Status after creation: suspended
Hello again. I’m a new coroutine.
Status after resuming: dead
So what happened there? Let’s take a look:
What does all this mean? It’s quite simple - Immediately after a coroutine is created, the code is “suspended” until you instruct Lua to resume it (through coroutine.resume). Once you have done so, the coroutine is run until it reaches one of two things:
Once it has reached one of these things, the coroutine will immediately pull out of the function and resume the ancestor code (we refer to it as an ancestor), which in this case is printing the status of the coroutine. Because the coroutine has finished all of the code in the function, the status returned by coroutine.status is “dead” - We can no longer make use of it. There are two other values that can be returned by coroutine.status, but you can check them out in the function definition index.
As I was saying earlier, a call to coroutine.resume will run the encapsulated function until it hits either a yielding point or the end of the function. A yielding point can be created with the function coroutine.yield, and it at this point that the true power of coroutines becomes evident. Let’s take a look at an example:
function myCoroutine ( )
print(“I am in the coroutine. Let’s yield now.”)
coroutine.yield( )
print(“I am back in the coroutine. End of function.”)
end
local co = coroutine.create(myCoroutine)
coroutine.resume(co)
print(“Status: “..coroutine.status(co))
coroutine.resume(co)
print(“Status: “..coroutine.status(co))
Output:
I am in the coroutine. Let’s yield now.
Status: suspended
I am back in the coroutine. End of function.
Status: dead
Let’s go through this step by step:
So essentially, coroutine.yield will stop the progress of the function and exit to its ancestor. Once the coroutine is resumed again though, it will pick up directly where it left off.
Now that you know how to stop and resume a coroutine, let’s take a look at how we can get values into and out of our functions. If you take a look at the definitions of coroutine.yield and coroutine.resume, you’ll see that you can pass some optional arguments. Let’s make a practical application of this:
function compareAges ( name, age )
print(“Hello, “..name..”!”)
print(“I am “.. (age<5 and “older” or “younger”) .. “ than you.”)
local your_response = coroutine.yield(“I’ll pass this back to my ancestor”)
print(“You have responded with: “..your_response)
end
local co = coroutine.create(compareAges)
local status, arg = coroutine.resume(co, “Bubba”, 17)
print(“The coroutine yielded and returned: “ .. arg)
coroutine.resume(co, “Sup.”)
Output:
Hello, Bubba!
I am younger than you.
The coroutine yielded and returned: I’ll pass this back to my ancestor
You have responded with: Sup.
There are a few new things going on here. The first is that you are providing coroutine.resume and coroutine.yield with a few extra arguments. This allows you to pass these arguments to and from the function at whichever point you wish. If you have just created the coroutine and call coroutine.resume(co, …), you will pass those arguments in as if they were in a function call. If you do so later in the function through a coroutine.yield, the arguments will be returned by the function itself.
Another new thing that we see here is the “status” variable returned by coroutine.resume. This is a boolean which signifies the success of the call - if there were any errors in the function, it will return false. Otherwise it will return true.
Now before we continue to multi-tasking, allow me to tell you a little bit about os.pullEvent.
Although os.pullEvent is doubtlessly a useful function, and is used constantly used throughout a large variety of programs, for this tutorial we need to take a closer look under the hood. If you extract the contents of the ComputerCraft.zip file and then navigate to the lua folder, you will see a file entitled “bios.lua”. If we open that file, you’ll see something very interesting defined there:
function os.pullEventRaw( _sFilter )
return coroutine.yield( _sFilter )
end
Yup. That’s right. Every time you pull an event in ComputerCraft, you’re actually using coroutines. That magical method which we are all so familiar with is now a bit less mysterious. So for the purposes of this tutorial, whenever you see os.pullEvent, keep in mind that it is nothing more than a call to coroutine.yield.
Now that you know how to use coroutines, let’s take a look at how this can apply to actual multi-tasking. Now when I say multi-tasking, please keep in mind that in fact, multi-tasking in Lua is still sequential - in other words, multi-tasking is not multi-tasking. But it is fast enough that we’ll notice no difference between the two, so let’s go ahead and make a little example.
To start, let’s make a regular function which will capture character events and print them to the screen.
local function getChars()
local termX, termY = term.getCursorPos()
while true do
local e = {os.pullEvent()}
if e[1] == "key" and e[2] == keys.enter then
return true
elseif e[1] == "char" then
term.setCursorPos(termX, termY)
term.write(e[2])
termX = termX + 1
end
end
end
Now that we have that done, let’s make another function which takes the characters and saves them to a file.
local function saveChars()
local file = fs.open("test_file", "w")
while true do
local e = {os.pullEvent()}
if e[1] == "key" and e[2] == keys.enter then
term.clear() term.setCursorPos(1,1) term.write("Saved the file.")
file.close()
elseif e[1] == "char" then
file.write(e[2])
end
end
end
So far we have two functions - one that gets characters and writes them to a screen, and another that takes characters and saves them to a file. Let’s use coroutines to run these two functions at the same time.
local getC = coroutine.create(getChars)
local saveC = coroutine.create(saveChars)
local evt = {}
while true do
coroutine.resume(getC, unpack(evt))
coroutine.resume(saveC, unpack(evt))
if coroutine.status(getC) == "dead" then
break
end
evt = {os.pullEvent()}
end
You can grab the full code here.
Go ahead and give that a try. What should happen is:
Cool! We’ve achieved a simplistic form of multi-tasking. Now go ahead and make your own use of coroutines.
Word Index
Ancestor - The “parent” of the coroutine. In other words, the function or body of code that calls coroutine.resume.
Block - A chunk of Lua code; usually a function, control structure (such as loops), or variable definition
Multi-Tasking - Running two or more chunks of code at the same time. In the context of Lua however, we do not run two chunks of code at exactly the same time but rather switch back and forth between the two quickly enough that it does not matter.
Yielding Point - The point at which Lua steps out of a function and hands over control to its ancestor.
Function Index
coroutine.create( function )
Creates and returns a coroutine.
coroutine.resume( coroutine, ...)
Resumes a coroutine. Any additional arguments to the coroutine itself will be returned by the yielding point in the function. Returns any arguments provided by coroutine.yield.
coroutine.status ( coroutine )
Gets the status of a coroutine. Can return four values:
coroutine.yield ( ... )
Stops running any code in a function and returns to its ancestor, where it can execute other code. Once the function is resumed again, it coroutine.yield will return any arguments provided to it by coroutine.resume.