Coroutines

You are already familiar with the idea of a subroutine. Control is transferred to some separate section of code which executes to completion. Then control is returned back from whence it came. The separate section of code is subordinate to the calling code, hence the name subroutine. Coroutines redefine the relationship between two sections of code to make them more like peers. Figure 6-2(a) shows a loop with two coroutines passing control back and forth between them.

Figure 6-2. Two Coroutines

Coroutine 1 consists of two blocks of code, C1a and C1b, similarly for coroutine 2. Coroutine 1 transfers control after C1a to coroutine 2 which in this case picks up execution at the beginning of C2b and continues with C2a and then transfers control back to coroutine 1. When control is transferred to a coroutine the execution always continues at the point the coroutine was last at (except the first time it is called in which case it starts at the top of the coroutine). You might ask why not simply include the blocks inline as in Figure 6-2(b). The answer is the same as for subroutines, a coroutine might be called from multiple places or, even if called from only one place, the resulting code can be clearer.

An example of the use of coroutines is to implement the idea of generators for loops. Here is an example (in Perl) of iterating over a list of names.

foreach my $k ("tom", "dick", "harry")
{
    print "not for any $k\n";
}

The list could instead be produced from a function call. We can talk about a function that generates a list and a loop that iterates over or consumes the list. This Perl example consumes one value from the list for each iteration. If we had a function generating the list it would construct the complete list in memory before the loop started. This could consume a lot of memory. A more efficient way would be to have the generating function and the consuming loop run concurrently. The generator generates a value in the list and returns it to the consumer. The consumer calls back to the generator for the next value. The generator and consumer are running as coroutines.

Some languages have included generators directly as a language feature. Here is an example from the CLU language [CLU].

start_up = proc()
    outstream:stream := primary_output()
    for s:string in get_hello_world() do
        stream$putl(outstream,s)
      end
  end

get_hello_world = iter() yields(string)
    while (true) do
        yield ("Hello, World!")
      end
  end

In CLU generators are a special type of procedure called an iterator or iter. The main function start_up() has a for loop that iterates the string variable s over the stream of strings produced by the generator get_hello_world(). The generator produces an infinite stream of "hello world" messages. The yield statement transfers control back to the calling loop. When the calling loop completes an iteration it will transfer control back to get_hello_world() after the yield statement and the while loop will go around again.

Coroutines provide a more general mechanism that you can use to implement patterns like this.

In the section called Pure FP and I/O in Chapter 1 I talked about lazy streams. They are another example of the producer/consumer relationship. You could implement lazy streams as coroutines. The compiler for a lazy functional language could be said to automatically convert functions to coroutines when there is a producer/consumer relationship.

Finally it is only a small step from coroutines to concurrent tasks. A set of tasks without pre-emptive scheduling is equivalent to a set of coroutines. Each task explicitly transfers control to another through a yield operation. If you add a timer to force the yield periodically then you have a proper pre-emptively scheduled concurrent system.

CML uses the call/cc operation to save the state of a running thread as a continuation. The continuations of the threads that aren't running are stored in a queue. When the current thread yields or is pre-empted a scheduler selects the next continuation from the queue and calls it to continue the thread. A timer is used to trigger a schedule of the current thread or a thread can yield when it performs some concurrent operation such as stopping to wait for a message. CML provides modified Basis library modules so that I/O can be safely preempted.

We can turn our view-point around now and use concurrent threads as a way to implement coroutines, lazy streams and any other kind of concurrent producer/consumer relationship. For example, a generator for a loop can be implemented as a thread that sends the list values as messages to a consuming thread. The generator will block until the consumer takes the next message.