37

I'm having trouble understanding this code... I was expecting something similar to threading where I would get an output with random "nooo" and "yaaaay"s interspersed with each other as they both do the printing asynchronously, but rather I discovered that the main thread seems to block on the first calling of coroutine.resume() and thus prevents the next from being started until the first has yielded.

If this is the intended operation coroutines, what are they useful for, and how would I achieve the goal I was hoping for? Would I have to implement my own scheduler for these coroutines to operate asynchronously?, because that seems messy, and I may as well use functions!

co1 = coroutine.create(function ()
        local i = 1
        while i < 200 do
                print("nooo")
                i = i + 1
        end
        coroutine.yield()
end)

co2 = coroutine.create(function ()
        local i = 1
        while i < 200 do
                print("yaaaay")
                i = i + 1
        end
        coroutine.yield()
end)

coroutine.resume(co1)
coroutine.resume(co2)
T. Bergemann
  • 51
  • 1
  • 5
kellpossible
  • 663
  • 1
  • 8
  • 19
  • 7
    Coroutines are not threads. They are thread-like, in that they are separate execution environments, but your program is the scheduler, and they're not preemptive. I would add a full answer, but my Lua is rusty, so I can't provide any code examples... – Mike Caron Feb 26 '11 at 17:09
  • is `coroutine.resume` blocking or non-blocking? – wick Oct 28 '22 at 16:27

3 Answers3

64

Coroutines aren't threads.

Coroutines are like threads that are never actively scheduled. So yes you are kinda correct that you would have to write you own scheduler to have both coroutines run simultaneously.

However you are missing the bigger picture when it comes to coroutines. Check out wikipedia's list of coroutine uses. Here is one concrete example that might guide you in the right direction.

    -- level script
    -- a volcano erupts every 2 minutes
    function level_with_volcano( interface )
    
        while true do
            wait(seconds(5))
            start_eruption_volcano()
            wait(frames(10))
            s = play("rumble_sound")
            wait( end_of(s) )
            start_camera_shake()
       
            -- more stuff

            wait(minutes(2))
        end
    
    end

The above script could be written to run iteratively with a switch statement and some clever state variables. But it is much more clear when written as a coroutine. The above script could be a thread but do you really need to dedicate a kernel thread to this simple code. A busy game level could have 100's of these coroutines running without impacting performance. However if each of these were a thread you might get away with 20-30 before performance started to suffer.

A coroutine is meant to allow me to write code that stores state on the stack so that I can stop running it for a while (the wait functions) and start it again where I left off.

VasiliNovikov
  • 9,681
  • 4
  • 44
  • 62
deft_code
  • 57,255
  • 29
  • 141
  • 224
  • 1
    Thanks, This answers my questions very well! – kellpossible Feb 26 '11 at 18:06
  • Perfect. I was wondering how I could do this in LUA, and now I know what to search for :). Thanks. – hiddensunset4 Jun 28 '11 at 03:13
  • 1
    @deft Just to help clarify, how does the above example work? As I understand lua has 3 primary functions to support coroutines: `create`, `resume` and `yield`. afaik `wait` isn't a built-in lua function. So is `wait` implemented by you? Where does the yielding happen? How do you ensure this coroutine `resume`'s after *x* amount of time has passed since the last `yield`? – greatwolf Aug 26 '11 at 21:51
  • 2
    @Victor, yes you implement it yourself. I added the extra stuff you could see how nice Lua coroutines can be for scripting. The main program (in C or Lua) creates a coroutine to run `level_with_volcano`. The `end_of`, `frames`, `seconds`, and `minutes` functions return a special type that `wait` understands. The `wait` function uses that special type to decide how long the coroutine needs to sleep and yields that value to the main program. The main program goes on about its business (drawing, updating, etc) and after enough time has passed it resumes the `level_with_volcano` coroutine. – deft_code Aug 26 '11 at 22:10
  • 3
    @deft_code Could you provide a simplified lua code of this particular use case? I've spent three hours trying to grok the concept of coroutines and now I feel very stupid because I still can't implement your example. – Nek Aug 26 '12 at 13:16
  • 3
    I see this hasn't been touched in over a year, but I'd also like to see the code implementation for the `wait` function. – ccjuju Dec 11 '12 at 06:48
  • 1
    @Nek, here is the example you wanted to see. – deft_code Sep 19 '14 at 04:35
  • @ccjuju, notifying you as well. – deft_code Sep 19 '14 at 04:36
  • Technically coroutines are threads `print(type(coroutine.create(function()end)))` --> thread – Moop Sep 30 '14 at 16:50
  • @Moop: I'll see your pedantry and raise you an obscure implementation detail. `coroutine.create` uses `lua_newthread` which returns a `lua_State`. So technically they're a `lua_State`. :) – deft_code Sep 30 '14 at 18:21
  • @deft_code it doesn't matter what `lua_newthread` returns, it matters what is left on the lua stack after calling `lua_newthread`. See: http://stackoverflow.com/questions/11379676/equivalent-of-lua-coroutine-create-in-c-using-lua-newthread – Moop Sep 30 '14 at 19:34
  • 2
    @Moop: TL;DR: Nuh Uh! The basic issue is I'm not calling "Lua threads" a thread. So your comment is true, they are technically Lua threads. To which I can reply, they aren't threads, they're coroutines. Yea Huh, Nuh Uh, Yea Huh, Nuh Uh... We're both right, but using "thread" with two different meanings. And now for a less sensible answer: `coroutine.create` is implemented by `luaB_cocreate` which returns a `lua_State` on the stack. The `type` function returns `"thread"` when it receives a `lua_State`. – deft_code Sep 30 '14 at 19:54
17

Since there have been a number of comments asking how to implement the wait function that would make deft_code's example work, I've decided to write a possible implementation. The general idea is that we have a scheduler with a list of coroutines, and the scheduler decides when to return control to the coroutines after they give up control with their wait calls. This is desirable because it makes asynchronous code be readable and easy to reason about.

This is only one possible use of coroutines, they are a more general abstraction tool that can be used for many different purposes (such as writing iterators and generators, writing stateful stream processing objects (for example, multiple stages in a parser), implementing exceptions and continuations, etc.).

First: the scheduler definition:

local function make_scheduler()
    local script_container = {}
    return {
        continue_script = function(frame, script_thread)
            if script_container[frame] == nil then
                script_container[frame] = {}
            end
            table.insert(script_container[frame],script_thread)
        end,
        run = function(frame_number, game_control)
            if script_container[frame_number] ~= nil then
                local i = 1
                --recheck length every time, to allow coroutine to resume on
                --the same frame
                local scripts = script_container[frame_number]
                while i <= #scripts do
                    local success, msg =
                        coroutine.resume(scripts[i], game_control)
                    if not success then error(msg) end
                    i = i + 1
                end
            end
        end
    }
end

Now, initialising the world:

local fps = 60
local frame_number = 1
local scheduler = make_scheduler()

scheduler.continue_script(frame_number, coroutine.create(function(game_control)
    while true do
        --instead of passing game_control as a parameter, we could
        --have equivalently put these values in _ENV.
        game_control.wait(game_control.seconds(5))
        game_control.start_eruption_volcano()
        game_control.wait(game_control.frames(10))
        s = game_control.play("rumble_sound")
        game_control.wait( game_control.end_of(s) )
        game_control.start_camera_shake()

        -- more stuff

        game_control.wait(game_control.minutes(2))
    end
end))

The (dummy) interface to the game:

local game_control = {
    seconds = function(num)
        return math.floor(num*fps)
    end,
    minutes = function(num)
        return math.floor(num*fps*60)
    end,
    frames = function(num) return num end,
    end_of = function(sound)
        return sound.start+sound.duration-frame_number
    end,
    wait = function(frames_to_wait_for)
        scheduler.continue_script(
            frame_number+math.floor(frames_to_wait_for),
            coroutine.running())
        coroutine.yield()
    end,
    start_eruption_volcano = function()
        --obviously in a real game, this could 
        --affect some datastructure in a non-immediate way
        print(frame_number..": The volcano is erupting, BOOM!")
    end,
    start_camera_shake = function()
        print(frame_number..": SHAKY!")
    end,
    play = function(soundname)
        print(frame_number..": Playing: "..soundname)
        return {name = soundname, start = frame_number, duration = 30}
    end
}

And the game loop:

while true do
    scheduler.run(frame_number,game_control)
    frame_number = frame_number+1
end
Mankarse
  • 39,818
  • 11
  • 97
  • 141
  • 1
    I know that this code isn't meant to be a full robust solution, but just wanted to caution that the implementation as-is will leak memory over time. The reason is the scheduler run function. This function checks for any "resumable" coroutines for the current frame and for each one, will ensure they are resumed. The problem is, it doesn't do anything to prevent the `script_container` from just growing unbounded. So make sure to clean up whatever was accounted for. – Ralph Caraveo Oct 29 '22 at 00:58
12
co1 = coroutine.create(
    function()
        for i = 1, 100 do
            print("co1_"..i)
            coroutine.yield(co2)
        end
    end
)

co2 = coroutine.create(
    function()
        for i = 1, 100 do
            print("co2_"..i)
            coroutine.yield(co1)
        end
    end
)

for i = 1, 100 do
    coroutine.resume(co1)
    coroutine.resume(co2)
end
dejf
  • 129
  • 1
  • 2