Oberon || Compiler & Tools || Library || Module Index || Search Engine
A simple and powerful coroutine scheme has been offered in Modula-2 by N. Wirth. The two basic operations (exported by the SYSTEM module) of Modula-2 are:
PROCEDURE NEWPROCESS(proc: PROC; addr: ADDRESS; size: CARDINAL; VAR new: ADDRESS); PROCEDURE TRANSFER(VAR source, destination: ADDRESS);NEWPROCESS creates a new coroutine with a stack starting at addr with size size. The coroutine starts execution by calling the parameterless global procedure proc. A handle to the new coroutine is returned in new.
The first disadvantage of NEWPROCESS is that coroutines must be parameterized by use of global variables. This leads to following typical layout:
VAR (* global variables *) crparams: CoroutineParameters; source: ADDRESS; (* current coroutine is called by this one *) newcr: ADDRESS; (* coroutine just created by NEWPROCESS *) PROCEDURE Coroutine; VAR myparams: CoroutineParameters; BEGIN (* copy parameters and return to calling coroutine *) myparams := crparams; TRANSFER(newcr, source); (* rest of coroutine *) END Coroutine; PROCEDURE SetupCoroutine(params: CoroutineParameters; proc: PROC); (* create coroutine and pass parameters *) BEGIN NEWPROCESS(proc, addr, size, newcr); crparams := params; TRANSFER(source, newcr); END SetupCoroutine;Each new coroutine is started just after creation to allow parameter copying. After getting parameters the coroutines transfer immediately back because the rest of the coroutine typically requires the existence of the other coroutines.
NEWPROCESS and TRANSFER are found in nearly every Modula-2 implementation and seem to be thus very portable at first sight. Nevertheless, the stack size required by the coroutine (parameter size of NEWPROCESS) is very implementation dependent.
TRANSFER has two variable parameters: source and destination. This allows two ways of allocating coroutine structures:
A special case is introduced by the main coroutine. The main coroutine is implicitly created during runtime start-off. The pointer to the coroutine structure of the main coroutine is set by the first TRANSFER operation. Thus, the source parameter is not an input parameter at least for the first TRANSFER-operation. This requires either the second case or the use of a prior invisible pointer to the coroutine structure of the main coroutine. Consequently, the source parameter can be omitted if at least the pointer to the main coroutine becomes visible.
The second case invalidates copies of earlier coroutine pointers as illustrated by following example:
VAR main, newcr: ADDRESS; PROCEDURE Coroutine; VAR cr: ADDRESS; BEGIN TRANSFER(cr, main); (* ... *) END Coroutine; PROCEDURE Setup; BEGIN NEWPROCESS(Coroutine, addr, size, newcr); TRANSFER(main, newcr); (* newcr is no longer valid *) END Setup;
NEWPROCESS and TRANSFER have been replaced by
PROCEDURE CRSPAWN(VAR newcr: COROUTINE); PROCEDURE CRSWITCH(dest: COROUTINE);and an interface module Coroutines (see below).
The main change is that coroutines declare themselves to be coroutines as illustrated by following example:
PROCEDURE Coroutine(myparams: CoroutineParameters; VAR newcr: SYSTEM.COROUTINE); BEGIN (* start as normal procedure *) SYSTEM.CRSPAWN(newcr); (* return to Setup *) (* execution continues here after CRSWITCH(newcr) *) END Coroutine; PROCEDURE Setup; VAR cr: SYSTEM.COROUTINE; BEGIN Coroutine(crparams, cr); (* ... *) END Setup;CRSPAWN performs following steps:
CRSPAWN returns like CRSWITCH if the newly created coroutine resumes execution.
Only a destination parameter is needed for CRSWITCH because the pointer to the coroutine (of type COROUTINE) does not change.
The pointer to the main coroutine is initialized during runtime start-off in the system interface module Coroutines:
DEFINITION Coroutines; (* run time interface to coroutines *) IMPORT SYS := SYSTEM; TYPE Coroutine = SYS.COROUTINE; CoroutineTag = POINTER TO CoroutineTagRec; CoroutineTagRec = RECORD END; VAR defaultsize: LONGINT; (* default initial stack size of a coroutine, the size of the activation record of the procedure calling CRSPAWN is added to the default; the default is taken if the second parameter of CRSPAWN is omitted *) tag: CoroutineTag; (* used for all coroutines *) main: Coroutine; (* is allocated during initialisation of this module and points to the main coroutine *) source: Coroutine; (* the last CRSWITCH operation was executed by this coroutine *) current: Coroutine; (* coroutine currently active *) END Coroutines.
PROCEDURE Produce(VAR newtoken: Token; VAR producer, consumer: Coroutines.Coroutine); VAR token: Token; BEGIN SYSTEM.CRSPAWN(producer); LOOP (* produce token *) newtoken := token; SYSTEM.CRSWITCH(consumer); END; END Produce; PROCEDURE Consume(VAR newtoken: Token; VAR producer, consumer: Coroutines.Coroutine); VAR token: Token; BEGIN SYSTEM.CRSPAWN(consumer); LOOP token := newtoken; (* consume token *) SYSTEM.CRSWITCH(producer); END; END Consume; PROCEDURE Setup; VAR token: Token; producer, consumer: Coroutines.Coroutine; BEGIN Produce(token, producer, consumer); Consume(token, producer, consumer); SYSTEM.CRSWITCH(producer); END Setup;
Ulm's Oberon library offers a couple of abstractions which base on coroutines. Tasks introduces the notion of tasks, task groups and schedulers. Each task group is controlled by a scheduler which is member of another task group. Task groups and schedulers form a tree with the main task as root.
Tasks need not to know about other coroutines and how to call CRSWITCH. Instead, they may tell what they are waiting for by the use of conditions, an abstraction offered by Conditions. Specific condition types are exported by StreamConditions, TimeConditions, EventConditions, Semaphores and other modules.
Usually, schedulers follow the standard scheduler interface of Schedulers which allows to add tasks dynamically. A simple round robin scheduler without priorities is exported by RoundRobin. A first task group which is managed by a round robin scheduler is created by the default implementation of SysModules.
Oberon || Compiler & Tools || Library || Module Index || Search Engine