Thoughts on Coroutines

I've been toying around with setjmp/longjmp (SJLJ)-based coroutines for the last week or so, and I like them. I was wondering about Casey's thoughts on them. I found them to be quite a cool construct. I think it may also have a place in HH's game code. Here's a snippet of code from Yossi Krenin's blog. Could they be covered in an HH episode?
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>
#include "coroutine.h"

typedef struct {
  coroutine* c;
  int max_x, max_y;
  int x, y;
} iter;

void iterate(void* p) {
  iter* it = (iter*)p;
  int x,y;
  for(x=0; x<it->max_x; x++) {
    for(y=0; y<it->max_y; y++) {
      it->x = x;
      it->y = y;
      yield(it->c);
    }
  }
}

#define N 1024

int main() {
  coroutine c;
  int stack[N];
  iter it = {&c, 3, 2};
  start(&c, &iterate, &it, stack+N);
  while(next(&c)) {
    printf("x=%d y=%dn", it.x, it.y);
  }
}

Here's the link to the implementation of start/next/yield here: https://www.embeddedrelated.com/showarticle/455.php
coroutines are fun but it's a very obvious case of moving complexity elsewhere and with the cost of not knowing what is running at any one time.

I would prefer stackless coroutines because 90% of the time you never yield anywhere but the coroutine function anyway.

These can be implemented yourself as a state machine where every variable you need preserved between calls must be saved explicitly in the context. That reduces all your longjumps into basic function calls.
ratchetfreak
coroutines are fun

Here's my preferred way to keep track of where you came from. What? Weren't we enjoying ourselves?

EDIT: I think we should encourage exploring constructs this way. To place on production code, however..

Edited by Abner Coimbre on
abnercoimbre
ratchetfreak
coroutines are fun

Here's my preferred way to keep track of where you came from. What? Weren't we enjoying ourselves?

EDIT: I think we should encourage exploring constructs this way. To place on production code, however..


The only thing I don't like about Duff's Device-based coroutines in that they hijack switch statements. I'd say that having a full-blown scheduler with SJLJ coroutines is asking for trouble. These kind of coroutines particularly good for implementing stateful generators. If this was to be pursued, a fiber-based system is probably easier to debug and maintain.
ratchetfreak
coroutines are fun but it's a very obvious case of moving complexity elsewhere and with the cost of not knowing what is running at any one time.

What do you mean?

Correct me if i'm wrong, but the way coroutines are used is -
  • you have a list of N active coroutines
  • and a list of M coroutines which are suspended (and are going to get activated after X ms).
  • all coroutines are "updated" at once at a certain place in your main loop, so its easy to measure their runtime. And are comparable to N function calls + the overhead of setjmp, longjmp, and whatever else goes into storing/restoring the state of the stack

  • You can always add a debug string to each coroutine so you always know the names of the functions which are being "stepped through".

    Its like having N suspended function calls that run on your main thread.
    Where each yield statement is like adding a state in the state-machine without having to write code to branch into that state, or write the "struct" to hold all the state data - its all "described" in that single function, with state stored on stack.

    It is like cheating. It certainly does feel like cheating.

    Since it's running on a single thread there are no "surprising" side effects, unless you go into complete crazy town with them.

    I find them invaluable when doing gameplay programming.
    Super useful for cutscenes in RPG-like games, or scripted events of any sort, in pseudocode:
    1
    2
    3
    4
    5
    6
    7
    8
    disable_controls();
    walk_left(5 tiles);
    play_animation("victory")
    wait(2sec);
    say_text("bla bla bla bla bla bla");
    create_item(x,y, "heart");
    walk_right(5 tiles);
    enable_controls();
    

    There's so much more that can be done with them.
    (calling a coroutine from coroutine, spawning multiple coroutines in parallel and waiting for both of them to finish before continuing on, etc)
    Super powerful construct for games.

    If they can be implemented in C in a way which is not cumbersome to use, it's a definite win (I use them in C# for gameplay all the time)

    Edited by pragmatic_hero on
    pragmatic_hero

    Since it's running on a single thread there are no "surprising" side effects, unless you go into complete crazy town with them.


    Part of the problem is that people will overuse the shiny new hammer they've been handed.

    At it's core they are little statemachines and to debug them you need to view them as statemachines. That requires both a human readable string to identify the state machine and a way to know what state the machine is in in the debugger.
    pragmatic_hero


    You can always add a debug string to each coroutine so you always know the names of the functions which are being "stepped through".

    Its like having N suspended function calls that run on your main thread.
    Where each yield statement is like adding a state in the state-machine without having to write code to branch into that state, or write the "struct" to hold all the state data - its all "described" in that single function, with state stored on stack.

    It is like cheating. It certainly does feel like cheating.

    Since it's running on a single thread there are no "surprising" side effects, unless you go into complete crazy town with them.

    If they can be implemented in C in a way which is not cumbersome to use, it's a definite win (I use them in C# for gameplay all the time)

    It's good to know that coroutines have a use-case in games. I thought this kind of code is easy to write and debug once you get used to inspecting a machine stack-based state machine. On the implementation, this code depends on setjmp, which is used to save/restore the $sp, $fp and $pc. Their initial change is made through inline assembly (sorry MSVC users). The alternative to inline assembly is to write start() in assembly. yield() and next() can be implemented each in a few lines of C. My addition was to pass arguments via varargs. It makes it look like the sample Python code at the link, but it can segfault if arguments are missing.
    ...written in C# (unity) or Lua (c++ engine)...
    Before I looked at Lua the first time, I never really got what the big deal was. However, Lua, and my own attempts at implementing a production scripting language in C, changed my mind on Coroutines in a big way:

    I see most people think of and use coroutines as a concurrency mechanism. This never seemed a particularly compelling construct to me for that use case. They are, however, great at keeping state information around in the background, thus they make for very efficient iterators. Using the coroutine metaphor, an iterator can be executed entirely within the context of a single function call, thereby avoiding those awful implementations where the previous state needs to be passed into the iterator function *again* on each round of the loop.

    Something about the concept of keeping the internal state of the iterator/coroutine on its own separate stack seems appealing to me. (I know iterators in C++ sort-of allow for similar mechanics, but in a much more verbose way...)
    I remember reading somewhere that coroutines were proposed for C++17. Not sure what the status is on that, but knowing the C++ committee, I'm not too optimistic..

    I experimented with implementing coroutines for my game project, but ended up going with timers instead. I didn't really need the stack space or the context switch features from coroutines. Instead, what I wanted was a way to schedule reasonably simple, short events without managing extra state or creating animations for them. For example, fade-ins, cross-fades, scripted movements, etc.

    So basically, I can write something like:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    float duration = 1.f;
    float interval = 1.f/60.f;
    float from = 0.f;
    float to = 1.f;
    
    fsTimer fadeInTimer(duration, interval);
    timer.callback = [&, from, to](fsTimer *timer) {
        float t = timer->t / timer->duration;
        t = fsClamp(t, 0.f, 1.f);
        float alpha = fs_lerp(from, t, to);
        aColor.alpha = alpha;
    };
    fadeInTimer.start();
    


    When start is called on the timer, it is added to a global list of timers that is iterated through and updated at the end of each frame (and gets removed from the list once its duration is up). This has been working out really well for me so far, and saved me from trying to hack together a coroutine implementation. :)

    Edited by Flyingsand on