MAINSAIL Language Manual, Chapter 21

previous   next   top   complete contents   complete index   framed top   this page unframed


21. Coroutines

A coroutine is a context that preserves the state of a PROCEDURE so that its execution may be resumed at the preserved state by a PROCEDURE in some other coroutine.

A coroutine may be thought of as a “thread of execution” that progresses independently of other threads of execution in an interleaved fashion. Thus, a coroutine executes for a while and then explicitly resumes some other coroutine. That new coroutine executes for a while, and then explicitly resumes another coroutine, perhaps the one that resumed it.

Most simple algorithms are best phrased without the use of coroutines. However, algorithms that require coordination among several operations of which the coordinating program does not know many details, or programs that wish to give the appearance of simultaneous execution of unrelated operations, can often make good use of coroutines.

In the absence of the MAINSAIL STREAMS package, coroutines must explicitly resume other coroutines; i.e., coroutines do not execute in parallel, and there is no automatic resumption of coroutines. However, a program may resume coroutines in such a way as to give the user the illusion of parallel execution. The MAINSAIL STREAMS package provides a Scheduler coroutine that does resume the coroutines under its control more or less automatically, and is simpler to use than explicit resumption of coroutines when the exact order in which coroutines are resumed does not matter; see Section 3.3 of the MAINSAIL STREAMS User's Guide.

21.1. Coroutine Facilities

System PROCEDUREs are provided to create, resume, and kill coroutines. MAINSAIL puts no limit on the number of coroutines, other than the limits implied by the size of memory on the target system. A PROCEDURE call stack is allocated for each coroutine to provide storage for the coroutine's PROCEDURE invocations.

Table 21–1 lists system PROCEDUREs, macros, and variables that deal with coroutines.

Table 21–1. System PROCEDUREs, Variables, and Macros for Coroutines
Identifier Function
$createCoroutine create a coroutine
$resumeCoroutine continue or start execution in a coroutine
$killCoroutine get rid of a coroutine
$killedCoroutine determine whether a coroutine has been killed
$moveCoroutine change location of coroutine in coroutine tree
$findCoroutine return a POINTER to a coroutine record, given its name
$thisCoroutine current coroutine
$exceptionCoroutine coroutine in which an exception occurred
$okToMixCoroutinesAndForeignCalls determine compatibility of coroutines with Foreign Language Interface

A coroutine is created by means of a call to $createCoroutine. The data section and the name of the PROCEDURE (the initializing PROCEDURE) in which the new coroutine is to start execution are specified in the call. A call to $resumeCoroutine is used to transfer execution to the new coroutine; the new coroutine may then use $resumeCoroutine to transfer control to the original (or any other) coroutine. When a coroutine determines that another coroutine will no longer be used, it may kill the other coroutine with a call to $killCoroutine, provided that the coroutine is not itself or one of its ancestors. A coroutine may kill itself by specifying the delete bit in a call to $resumeCoroutine.

The exception $coroutineExcpt is raised if a coroutine attempts to return from its initializing PROCEDURE. The initializing PROCEDURE must terminate by calling $resumeCoroutine.

Example 21–2 shows a use of coroutines. A number of MODULEs of the CLASS generatorCls each contain a PROCEDURE generateNodes that consists of a loop that produces pieces of information (nodes). It is not predictable ahead of time exactly in what order the nodes become available from which MODULEs; if the order were predictable, then it would probably be easier to write the algorithm without coroutines. The INITIAL PROCEDURE of the MODULE processor allocates the generator MODULEs and coroutines. It calls a PROCEDURE findReadyCoroutineOnList (not shown), which figures out (somehow) which of the generator coroutines is ready to produce a node. processor then resumes that coroutine, which passes back its node in processor's interface variable nextNode by setting nextNode and then resuming processor's coroutine. If no generator coroutine is ready, the scheduling loop in processor times out 3 seconds, and then looks again for a generator ready to produce a node.

Example 21–2. Generator/Processor Coroutines
Header file:

CLASS generatorCls (
    
PROCEDURE generateNodes;
);

MODULE processor (
    
POINTER(nodenextNode;
    
POINTER($coroutineprocessorCo;
);

...
##########################################################
A generatorCls module:
...

MODULE(generatorCls) ...; # this module is of generatorCls

PROCEDURE generateNodes;
BEGIN
...
DOB ...
    # 
Use PROCESSOR's interface variable nextNode for
    # 
passing information between coroutines
    
nextNode := ...;
    
$resumeCoroutine(processorCo);
    ...
    
END;
END;

...
##########################################################
The module PROCESSOR:

...

CLASS generatorLstCls (
    
POINTER(generatorClsgen;
    
POINTER($coroutinegenCo;
    
POINTER(generatorLstClsnext;
);

...

INITIAL PROCEDURE;
BEGIN
INTEGER i;
STRING s;
POINTER(generatorLstClsgenLst,p;

processorCo := $thisCoroutine;
Set up a bunch of generatorsgiven a list of module
names
WHILE processorModuleNames DOB
    
read(processorModuleNames,s);
    
p := new(generatorLstCls);
    
p.gen := new(s); # Allocate generator module
    
p.genCo := $createCoroutine(p.gen,"generateNodes");
    
p.next := genLstgenLst := p END;

DOB ...
    # 
Scheduling loop for generator coroutines:
    
DOB IF NOT p := findReadyCoroutineOnList(genLstTHENB
            
$timeout(3L); CONTINUE END;
        
$resumeCoroutine(p.genCo);
        ... 
process nextNode here ...
        
p := p.next END END;
...
END;

...

In general, coroutines must transmit data to one another by means of outer or interface variables, since there is no way to pass parameters into or out of a coroutine using, e.g., $resumeCoroutine. In an environment (like a program making heavy use of the STREAMS package) where many kinds of actions may cause some other coroutine to be resumed, it is best to call $resumeCoroutine as soon as possible after setting the outer or interface variables holding the data to be transmitted, to ensure that no other coroutine using the same variables changes the variables' value in the interim.

Coroutines provide stacks for PROCEDURE invocations, but do not create new data sections for MODULEs; i.e., PROCEDUREs executing in different coroutines may access the same data section. If a separate data section is desired, it must be explicitly created with the system PROCEDURE new and the newly created data section specified as an argument to $createCoroutine (as shown in Example 21–2).

21.2. Coroutine Implementation

A coroutine consists of a stack to hold the PROCEDURE frames and a dynamic record of the predeclared CLASS $coroutine that contains information about the coroutine.

A tree structure is imposed on coroutine records based on a parent-child relationship. Each coroutine record has a link $up that points to its parent coroutine record, which is initially the coroutine that created it. The link $down points to the first child, which is initially the first-created (oldest) child. $right points (by default) to the next younger sibling, and $left to the next older sibling. Example 21–3 shows a coroutine tree, where the letters represent coroutine records, the vertical bars represent the $up and $down links, and the dashes represent the $left and $right links. Only the $up links for the oldest children are shown; nodes c, d, e, and f have $up links to node a, h to f, j to g, and p, q, and r to l. a's $up link is NULLPOINTER since it is the root coroutine; this coroutine is called MAINSAIL and is created when the MAINSAIL runtime system initializes itself.

Example 21–3. Coroutine Tree
a
|
b--c--d--e--f
|  |  |     |
m  n  k     g--h
      |     |
      
l     i--j
      |
      
o--p--q--r

The $up, $down, $left, and $right links depend on the order of coroutine creation (unless coroutines have been moved about in the coroutine tree, e.g., with the system PROCEDURE $moveCoroutine). In addition, $coroutine records are maintained on a “dynamic” list by means of the $prev and $next links. Each living coroutine appears exactly once on this list. Each time a coroutine is resumed, it is moved to the head of the list. The head of the list is pointed to by the predeclared variable $thisCoroutine, so that $thisCoroutine points to the record for the currently executing coroutine, and $thisCoroutine.$next points to the coroutine that most recently resumed the current one, if that coroutine is still living (this may not be a coroutine within the current application, however; creation of coroutines by the MAINSAIL runtime system and searches for exception handlers also reorder the dynamic coroutine list). The list is thus ordered from most recently resumed to least recently resumed (where a search for a handler in a coroutine is considered a resumption of the coroutine); newly created but not yet resumed coroutines are put at the tail of the list.

The $coroutine record includes the following fields of interest to the user:

Modifying fields other than $userHook or accessing undocumented fields of a $coroutine record has undefined effects.

21.3. Coroutines and Exceptions

Unhandled exceptions are propagated up the coroutine tree through the $up link, so that a coroutine can handle exceptions in itself and its descendants but not, for example, in its siblings or ancestors.

If an exception is raised in a coroutine (the raisee coroutine), any active handlers in the raisee coroutine are given control as usual. If none of them handles the exception, the exception is simulated in the raisee coroutine's parent to give an opportunity for active handlers there to handle the exception. If the exception is still not handled, the exception is simulated in the raisee coroutine's grandparent (all traces of the simulated exception in the parent are erased), and so forth. This propagation continues until either the exception is handled, or a $raiseReturn occurs in the coroutine to which the exception has been propagated, or there is no handler in the root node MAINSAIL.

The system macro $exceptionCoroutine returns a POINTER to the raiser coroutine (the coroutine in which $raise was called) for the current exception (different from the raisee coroutine only if the exceptionCoroutine argument to $raise denoted a coroutine other than the raiser coroutine).

$raiseReturn in a coroutine to which the exception has been propagated first erases any trace of the propagated exception, then resumes the raiser coroutine. If $raiseReturn is not allowed by the original exception (e.g., an implicit exception such as divide-by-zero or $raise with $cannotReturn set in ctrlBits), an error occurs if the raiser coroutine is ever resumed, whether by $raiseReturn or by $resumeCoroutine. The program should kill the raiser coroutine if it can never be resumed.

It is possible to resume the raiser coroutine of an active exception by using $resumeCoroutine, provided that the exception does not have the $cannotReturn bit set in $exceptionBits. In this case, a subsequent $raiseReturn in the handler for the exception may resume the coroutine at a point other than the raiser coroutine's call to $raise. For example, the following program:

BEGIN "coex"

POINTER($coroutineinitCo,raiser,raisee;

PROCEDURE raiserProc;
BEGIN
BITS xxx;
write(logFile,
    "
In raiserProcbefore raising exception" & eol);
$raise("Exception!","","",NULLPOINTER,'0,xxx,raisee);
write(logFile,
    "
In raiserProcafter call to $raise" & eol);
$resumeCoroutine(raisee);
write(logFile,
    "
In raiserProcafter $resumeCoroutine" & eol);
$resumeCoroutine(initCo);
END;



PROCEDURE raiseeProc;
$HANDLEB
    
write(logFile,
        "
In raiseeProcwithin $HANDLE statement" & eol);
    
$resumeCoroutine(initCoEND
$WITHB
    
IF $exceptionName NEQ "Exception!" THEN $raise;
    
write(logFile,
        "
In handlerhandling Exception!," & eol &
        "
about to resume raiser coroutine" & eol);
    
$resumeCoroutine(raiser);
    
write(logFile,
        "
In handlerabout to call $raiseReturn" & eol);
    
$raiseReturn END;



INITIAL PROCEDURE;
BEGIN
initCo := $thisCoroutine;
    # 
Coroutine from which program was run
raiser := $createCoroutine
        (
thisDataSection,"raiserProc","raiser");
raisee := $createCoroutine
        (
thisDataSection,"raiseeProc","raisee");
$resumeCoroutine(raisee); # Let raisee set up its handler
$resumeCoroutine(raiser); # Let raiser raise exception
$killCoroutine(raiser);
$killCoroutine(raisee);
END;

END "coex"

has the following output:

In raiseeProcwithin $HANDLE statement
In raiserProcbefore raising exception
In handlerhandling Exception!,
about to resume raiser coroutine
In raiserProcafter call to $raise
In handlerabout to call $raiseReturn
In raiserProcafter $resumeCoroutine

If the exception is handled, PROCEDUREs are aborted in the handling coroutine as usual, and execution continues in the handling coroutine. The raiser coroutine is not killed, but is left in a such a state that, if resumed, it continues from the call to $raise as if $raiseReturn had been called from a handler. Thus, execution can be resumed from a call to $raise handled by a handler in a different coroutine from the raiser coroutine. An error occurs if the raiser coroutine is resumed but the exception does not allow a $raiseReturn.

The use of $resumeCoroutine to resume a coroutine that propagated the current exception (before it is handled) has undefined effects.

The programmer should be careful to kill coroutines that will never be resumed in order to free the space occupied by the coroutines.

If $raiseReturn or an argumentless call to $raise occurs in some coroutine other than the raisee coroutine or the one to which the exception has been propagated, then that $raiseReturn or $raise applies only to the active exception in its coroutine (an error if there is no such exception); i.e., exceptions are maintained on a per-coroutine basis except for the propagation described above.

When a coroutine is killed, the informational exception $descendantKilledExcpt is raised in each of the coroutine's ancestors to inform the coroutines that their descendant has died. $descendantKilledExcpt is described in more detail in Section 39.2.

21.4. C Exceptions and MAINSAIL Coroutines

There are restrictions on the interactions between foreign code and MAINSAIL coroutines. The information below refers to C, but applies to any program language with features similar to those mentioned for C:


previous   next   top   complete contents   complete index   framed top   this page unframed

MAINSAIL Language Manual, Chapter 21