Publié à l'aide de Google Docs
Coroutines
Mise à jour automatique effectuée toutes les 5 minutes

Advanced Lua Topics - Coroutines

Tutorial Index

How to read this tutorial

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

Don’t forget about arguments

What is os.pullEvent really?

Multi-Tasking

Function/Word Index


How to read this tutorial

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.

Introduction: What are Coroutines?

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.


The Basics: Creating a Coroutine

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.


Resuming Coroutines - Let’s run some coroutines

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.


Getting the Status of our Coroutines

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:

  1. We created our coroutine, and set it to the variable simpleCo.
  2. We used coroutine.status to get the status of the coroutine - just after creation, it is “suspended”
  3. We resumed simpleCo using coroutine.resume.
  4. We printed out the status of simpleCo again - this time it’s “dead”

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:

  1. An explicit yielding point - we will encounter these later in the tutorial.
  2. The end of the function (at which point it yields implicitly).

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.

Yielding - Suspend our Coroutine

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:

  1. Create the Coroutine
  2. Resume it
  3. Inside of the function, we print “I am in the coroutine. Let’s yield now.” and call coroutine.yield
  4. We print out the status of the coroutine from outside of the function - it is “suspended”
  5. We resume the coroutine again and print that we are back inside the coroutine.
  6. We reach the end of the function and step back out to the ancestor, printing the new status which is “dead”

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.

Don’t forget about arguments

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.

What is os.pullEvent really?

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.


Multi-Tasking

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:

  1. You’ll see characters printed to the screen as you type them
  2. At the same time, the saveChars function will save them to a file
  3. Once you hit enter, the file will close and you can view it.

Cool! We’ve achieved a simplistic form of multi-tasking. Now go ahead and make your own use of coroutines.


Function/Word Index

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.