Lua Coroutines – Getting Started

Sometimes I run across a concept that’s really exciting, and I want to learn/know/master it, but my brain just throws up block after block, preventing me from truly learning it. Lua’s coroutines are kind of like that for me. I mean, think of the prospect of using ‘light weight threads’ in your programs. Lua’s coroutines aren’t ‘threads’ at all though, at least not in the usual ‘preemptive multitasking’ sense that one might think.

Lua coroutines are like programming in Windows 1.0. Basically, cooperative multi-tasking. If any one ‘thread’ doesn’t cooperate, the whole system comes to a standstill until that one task decides to take a break. So, some amount of work is required to do things correctly.

How to get started though?

Well, first of all, I start with a simple function:

local routines = {}

routines.doOnce = function(phrase)
  print(phrase)
end

routines.doOnce("Hello, World")
>Hello, World

A simple routine, that prints whatever I tell it to print. Fantastic, woot! Raise the roof up!!

Why did I go through the trouble of creating a table, and having a function, just to print? All will become clear in a bit.

Now, how about another routine? Perhaps one that does something in a loop:

routines.doLoop = function(iterations)
  for i=1,iterations do
    print("Iteration: ", i)
  end
end

routines.doLoop(3)
>Iteration: 1
>Iteration: 2
>Iteration: 3

Fantastic! Now I have one routine that prints a single thing, and exits, and another that prints a few numbers and exits.

Now just one last one:

routines.doTextChar = function(phrase)
  local nchars = #phrase

  for i=1,nchars do
    print(phrase:sub(i,i))
  end
end
>routines.doTextChar("brown")
>b
>r
>o
>w
>n

Now, what I really want to do is to be able to run all three of these routines “at the same time”, without a lot of fuss. That is, I want to allow the doOnce() routine to run once, then I want the doTextChar(), and doLoop() routines to interleave their work, first one going, then the next.

This is where lua’s coroutines come into the picture.

First of all, I’ll take the doOnce as an example. In order to run it as a corutine, there isn’t anything that needs to change about the routine. But, you do have to start using some coroutine machinery. Instead of calling the routine directly as usual, you call it using coroutine.resume(), and before that, you have to create something to resume, what Lua knows as a ‘thread’, using coroutine.create(). Like this:

local doOnceT = coroutine.create(doOnce)
coroutine.resume(doOnceT, "Hello, World");

This will have the same effect as just running the routine directly, but will run it as a coroutine. Not particularly useful in this case, but it sets us up for some further success.

The looping routines are a little bit more interesting. You could do the following:

local doLoopT = coroutine.create(doLoop)
coroutine.resume(doLoopT, 3)

That will have the same output as before. But, it won’t be very cooperative. The way in which a routine signals that it’s willing to give up a little slice of it’s CPU time is by calling the ‘coroutine.yield()’ function. So, to alter the original routine slightly:

routines.doLoop = function(iterations)
  for i=1,iterations do
    print("Iteration: ", i)
    coroutine.yield();
  end
end

Now, running will do exactly the same thing, but we’re now setup to cooperate with multiple tasks.

I will alter the ‘doTextChar()’ routine in a similar way:

routines.doTextChar = function(phrase)
  local nchars = #phrase

  for i=1,nchars do
    print(phrase:sub(i,i))
    coroutine.yield();
  end
end

OK, so now I have three routines, which are ready for doing some coroutine cooperation. But how to do that? The essential ingredient that is missing is a scheduler, or rather, something to take care of the mundane task of resuming each routine until it is done.

So, in comes the SimpleDispatcher:

local Collections = require "Collections"

local SimpleDispatcher_t = {}
local SimpleDispatcher_mt = {
  __index = SimpleDispatcher_t
}

local SimpleDispatcher = function()
  local obj = {
    tasklist = Collections.Queue.new();
  }
  setmetatable(obj, SimpleDispatcher_mt)

  return obj
end


SimpleDispatcher_t.AddTask = function(self, atask)
  self.tasklist:Enqueue(atask)	
end

SimpleDispatcher_t.AddRoutine = function(self, aroutine, ...)
  local routine = coroutine.create(aroutine)
  local task = {routine = routine, params = {...}}
  self:AddTask(task)

  return task
end

SimpleDispatcher_t.Run =  function(self)
  while self.tasklist:Len() > 0 do
    local task = self.tasklist:Dequeue()
    if not task then 
      break
    end

    if coroutine.status(task.routine) ~= "dead" then
      local status, values = coroutine.resume(task.routine, unpack(task.params));

      if coroutine.status(task.routine) ~= "dead" then
        self:AddTask(task)
      else
        print("TASK FINISHED")
      end
    else
      print("DROPPING TASK")
    end
  end
end

return SimpleDispatcher

Before explaining much, here is how I would use it:

local runner = SimpleDispatcher();

runner:AddRoutine(routines.doOnce, "Hello, World");
runner:AddRoutine(routines.doLoop, 3)
runner:AddRoutine(routines.doTextChar, "The brown")

runner:Run();

Basically, create an instance of this simple dispatcher.
Then, add some routines to it for execution. AddRoutine() does not actually start the routines going, it just queues them up to be run using coroutine.resume().
Lastly, call ‘runner:Run’, which will start a loop going, resulting in each of the tasks being called one after the other. This will result in the following output:

>Hello, World
>1
>T
>2
>h
>3
>e
>
>b
>r
>o
>w
>n

Basically, each time a routine calls ‘coroutine.yield()’, it’s giving up for a bit, and allowing the dispatcher to call the next routine. When it comes back around to calling the routine that gave up a slice of CPU time, it will resume from wherever the ‘yield()’ was called. And that’s the really part of coroutines!

I find the easiest way to think about coroutines is to simply code something, as if it were going to be an independent ‘thread’. Then I throw in a few choice yield() calls here and there to ensure the thread cooperates with other threads that might be running.

There are quite exotic things you can do with coroutines, and all sorts of frameworks to make working with them better/easier, and perhaps more mysterious.

For my simplistic pea brain though, this is a good place to start. Just taking simple routines, and handing them to the dispatcher.

That dispatcher is quite a nice little piece of work. If you think about it, it’s the equivalent of the scheduler within any OS. Assuming your Lua code is running in a single threaded process, being able to do your own scheduling is quite a powerful thing. You can add all sorts of exotics like aging, and thread priority. You can add timers, and I/O event or what have you. The beauty is, the scheduler can become as complex as you care to make it, and your coroutine code does not have to do anything special to adapt to it, just like with thread code in the OS proper.

So, there you have it. Getting started with Lua coroutines.

Now, if only I could combine socket pooling and this Dispatcher in some meaningful way. I wonder…

Advertisements

4 Comments on “Lua Coroutines – Getting Started”

  1. […] (~200 lines of code), with the help of a couple of libraries for handling bitwise operations and scheduling coroutines (to be able to read and write on a serial port at the same time). I had to go past a couple of […]

  2. aniruddhaa2013 says:

    Very nice!

  3. […] Summaries from my own archives Lua Coroutines – Getting Started […]

  4. criztianix says:

    In the example you use: require “Collections”, but i cannot find this module/library
    where i can find it?


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s