Multitasking and the event loop

Marco Maggi (Page updated on June 2003 with the new task package version) - With TCL it's possible to organise the code so that a sequence of event handler invocations can be viewed as the execution of a "subprocess" of the main script; this is done arranging access for the event handlers to a set of variables considered private for the subprocess. I'll name these entities "event loop processes" or tasks.

In this page I describe how this concept can be applied when we need to spawn and control multiple external processes from a TCL shell. I make use of the The task package, whose code is not duplicated in this page, but it's included in the script.


The concept is easy: since an event handler is a script and TCL is capable of building a script at runtime, we use a procedure as handler and we allocate a variable name reserved for the task:

    proc handler { varname channel } {
        upvar        $varname value

        # do something
    }

    set value 123

the variable name is handed to the procedure in the event handler script; for example: if the event is the readability of an I/O channel:

    fileevent $channel readable \
        [namespace code handler ::value $channel]

every time the channel becomes readable, the procedure [handler] can update the value of the global variable (note that the variable must be of static type: a global or namespace variable). If the variable is an array, more than one value can constitute the "state" of the task.

Obviously, at some point the channel will be closed and the handler procedure will be detached from the event loop. The state of the task must not be lost, so for example:

    proc handler { varname channel } {
        upvar $varname value

        ... 
        if { [eof $channel] } {
           close $channel
           terminate $varname
        }
    }

[handler] invokes the procedure [terminate] to take care of task finalisation; the fully qualified name of the state variable is handed as argument to the finalisation procedure.

The [handler] procedure is reentrant: the state of the event loop process is stored in a global variable that's accessed only in the sequence of event loop handlers evaluations.

It is good programming style to keep the event handler script itself as little as possible, and to code all the task logic in the handler procedure.


Now the multitasking example. A script executes external processes and reads data from them until they are terminated. To simulate an external process, we create the following TCL script:

    # sleeper.tcl --
    puts HELLO
    after 1000 { set ::forever 1 }
    vwait forever
    puts QUIT
    exit 0
    # end of file

the operations are: write "HELLO" to the standard output channel; wait for one second; write "QUIT" to the standard output channel; exit. We'll execute it with the commands:

    set cmd [list [auto_execok tclsh] ./sleeper.tcl]
    set id [open |$cmd {RDWR}]

The controlling script has a page for itself: Multitasking and the event loop: the script. It's a TCL+TK script that let's us create multiple event loop processes, each of which starts a sequence of "sleeper.tcl" scripts. Every time a new "sleeper.tcl" script is executed a counter is incremented, so we can track how many scripts each task has executed so far.

Tasks can be stopped, resumed and terminated. When exiting: the script waits for the termination of all the event loop processes, and each process waits for the termination of the running "sleeper.tcl" script.

To run it: create the two scripts "sleeper.tcl" and "controller.tcl", placing them in the same directory; then execute:

    $ tclsh controller.tcl