Agrim Mittal a.k.a hitman

Getting to know Lua (Part 4)

This post is in continuation of previous post Getting to know Lua (Part 3)

Session 4: Coroutines

A coroutines is like thread, with its own stack, its own local variables, and its own instruction pointer; but shares most globals and mostly anything else with other coroutines.

The main difference between coroutines and threads is that, a program with threads runs several threads concurrently whereas coroutines are collaborative. A program with coroutines is, at any given time, running only one of its coroutines and this running coroutine only suspends its execution when it explicitly requests to be suspended.

Basics

Lua offers all its coroutine functions packed in the coroutine table

co = coroutine.create(function ()
       print("Hello from the coroutine")
     end)

print(co)                 -- thread: 0xa57670

A coroutine table provides many methods. We can list all methods using,

for k, v in pairs(coroutine) do
    print(tostring(k))
end
running
create
resume
status
isyieldable
wrap
yield

Onto rough waters

We can create a coroutine using create function which only creates a new coroutine and returns a handle to it but doesnot start the coroutine.

A coroutine can be in any of the three different states:

  • Suspended
  • Running
  • Dead

When we create a coroutine, it starts in the suspended state. The status of a coroutine can be checked by using status function,

print(coroutine.status(co))     -- suspended

We can execute a coroutine by calling a resume function and passing a first argument as thread returned by the create function.
The function resume starts coroutine execution,

coroutine.resume(co)            -- "Hello from the coroutine"

In this state coroutine executes and leaves it in dead state

print(coroutine.status(co))    -- dead

Until now, coroutines look like nothing more than a complicated way to call functions. The real power of coroutines stems from the yield function, which allows a running coroutine to suspend its execution so that it can be resumed later.

co = coroutine.create(function ()
       for i=1,5 do
         print("co ", i)
         coroutine.yield()
       end
     end)

coroutine.resume(co)           -- co 1
print(coroutine.status(co))    -- suspended
coroutine.resume(co)           -- co   2
...
coroutine.resume(co)           -- co   5
coroutine.resume(co)           -- prints nothing

print(coroutine.resume(co))    -- false   cannot resume dead coroutine

When a coroutine yields, the corresponding resume returns immediately, even if the yield happens inside nested function calls (that is, not in the main function, but in a function directly or indirectly called by the main function). It can be demonstrated as

function foo ()
    print("in foo")
    coroutine.yield("foo coroutine yield")
    print "done"
end

co = coroutine.create(function ()
       foo()
       print("in co body")
     end)

print("main", coroutine.resume(co))
print("main", coroutine.resume(co))

This outputs

in foo
main    true    foo coroutine yield
done    --[[
            This print after we call the second resume because yield returned the function at an early stage
        ]]--
in co body
main    true

Resume runs in protected mode. Therefore, if there is any error inside a coroutine, Lua will not show the error message, but instead will return it to the resume call.

We can also pass extra arguments resume that are supplied to the coroutine main function.

A more cool example can be taken from Lua Manual,

function foo (a)
    print("foo", a)
    return coroutine.yield(2*a)
end

co = coroutine.create(function (a,b)
        print("co-body", a, b)
        local r = foo(a+1)
       print("co-body", r)
       local r, s = coroutine.yield(a+b, a-b)
       print("co-body", r, s)
       return b, "end"
    end)

print("main", coroutine.resume(co, 1, 10))
print("main", coroutine.resume(co, "r"))
print("main", coroutine.resume(co, "x", "y"))
print("main", coroutine.resume(co, "x", "y"))

The output of this program is rather interesting and illustrates the above discussion in more detailed manner,

co-body 1       10
foo     2
main    true    4
co-body r
main    true    11      -9
co-body x       y
main    true    10      end
main    false   cannot resume dead coroutine

A coroutine can terminate its execution in two ways: if the main funtion returns or if there is an unexpected error. In first case, resume returns true plus the values returned by main function and in second case it return false along with an error message.

A simple example

function foo (a)
    print("in foo")
    return coroutine.yield(2*x)
end

co = coroutine.create(function ()
        print("in co-body")
        local r = foo(1)
        print(r)
    end)

print("main", coroutine.resume(co))
print("main", coroutine.resume(co))

Outputs

in co-body
in foo
main    false   a.lua:3: attempt to perform arithmetic on a nil value (global 'x')
main    false   cannot resume dead coroutine

There is another function same as create, known by the name wrap. It also creates a coroutine, but instead of returning the coroutine, it returns a function that, when called, resumes the coroutine. Any arguments passed to this function are passed as extra arguments to resume. wrap returns all the values returned by resume, except the first one (the boolean error code). Unlike resume, wrap does not catch errors; any error is propagated to the caller.

co = coroutine.wrap(function ()
        coroutine.yield("Yay!")
    end)

print("main", co())      -- main    Yay!
print("main", co())      -- main

Roberto writes in book,

Lua offers what I call asymmetric coroutines. That means that it has a function to suspend the execution of a coroutine and a different function to resume a suspended coroutine. Some other languages offer symmetric coroutines, where there is only one function to transfer control from any coroutine to another.

Coroutines are a kind of collaborative multithreading

References