The SWL thread system extends Chez Scheme to provide explicit concurrency to both SWL and its applications. A variety of user interface problems are elegantly solved by providing both the SWL implementer and the SWL application writer with access to multiple independent threads of control. Judicious use of multiple threads improves the modularity of both SWL and many of its applications.
Chez Scheme provides a sufficient basis for implementing a thread system entirely in Scheme. The Chez Scheme notions of interrupt handling and timed preemption provide a mechanism for interrupting execution of a Scheme program in a controlled way.
Scheme's general purpose control primitive call-with-current-continuation (call/cc) provides a means for defining new dynamic execution contexts, or continuations, and for the transfer of control from one continuation to another. When augmented with appropriate additional information, a Scheme continuation becomes a thread, or independently schedulable continuation. The SWL thread system provides for the creation and destruction, suspension and resumption, synchronization, and inspection of multiple threads of control.
The major design goals for the thread system are summarized here:
While these goals have largely been achieved, we note two significant exceptions now. First, the Chez Scheme syntactic form fluid-let does not function with multiple threads as might be expected. Second, to achieve the other goals we have tolerated some dependence on Chez Scheme internals, i.e., the use of certain undocumented Chez Scheme system mode primitives. These issues are fully discussed below.
The rest of this chapter is a user's guide to the SWL thread system. The the thread system adds a number of API procedures and syntax, data stuctures, and global variables. Separate sections discuss the effects on standard language constructs and dependencies on Chez Scheme internals.
The thread system adds a number of user interface procedures to Chez Scheme.
ps returns a list of values, one for each thread of the current thread group, where each value provides information about the thread. A value is a list of atoms (strings, symbols, or numbers), or sublists of two elements: a keyword symbol and an atom. The argument thread specifies which thread's values are returned. If omitted, thread defaults to the currently running thread.
The output of ps is intended for use as input to an interpreting procedure, such as pps.
ps-all returns a list of values, one for each thread in the system, where each value provides "raw" information describing the thread. See ps above for more details.
The output of ps-all is intended for use as input to an interpreting procedure, such as pps.
pps (short for "pretty ps") prints a status report for all threads. output-port determines where the report is printed. If omitted, it defaults the value of (current-output-port).
(run-queue 10 4) (sleep-queue)
[console(1)] app-q 0.01 server (starts 8)
[Fallback(2)] idle (priority -1) 0.01 server (starts 4)
[System Fallback(3)] idle (priority -1) 0.01 server (starts 1)
[Event Dispatcher(4)] ready (lowest priority) 0.01 server (starts 2499)
[Fallback(7)] idle (priority -1) 0.01 server (starts 998)
[Repl(9)] running 0.01 server (starts 9)
 (sleeping 0.099) 0.01 (starts 14)
In this example pps-all has been executed in a freshly started SWL system. The output shows that at present there are seven threads in total, which are clustered into four thread groups. The first group provides support for the conventional Chez Scheme command line interpreter. The second and third (single-thread) groups are internal to SWL. The forth group (of three threads) is allocated to the initial REPL, which is automatically created when SWL is started.
A thread attribute is either an atom or a list containing a keyword and a value. Certain attributes are optional may not appear for a given thread. The possible attributes are:
ps-num returns the number of threads currently in the system. The number of threads is affected by thread creation (via thread-fork and thread-fork-group) and by thread termination (via thread-kill).
thread->k returns the Scheme continuation of its thread argument. If the currently executing thread is passed, the return value is the continuation of the currently executing thread when it was last started.
thread-number returns the integer identifier of the argument thread, or the currently executing thread if no argument is supplied.
thread-self returns the currently executing thread. In the thread system, all Scheme code is executed within an associated thread.
thread-quantum-remaining returns the amount of execution time remaining before the currently executing thread is subject to preemption by the thread scheduler. The time quantum is a positive real number, typically a small fraction of a second, e.g., .01 second.
If the currently executing thread's time quantum has expired, and another thread of equal or higher priority is ready to run, the thread scheduler suspends the current thread (i.e., forces the thread to yield), and starts another one.
thread-find returns the thread corresponding to the integer identifier argument. If no such thread exists, thread-find returns #f.
Exceptions are execution events that alter the normal flow of control within a thread. There are several kinds of thread system exceptions: interrupts, resets, and errors. Different kinds of threads may handle these exceptions in different ways. One kind of thread that is frequently used in SWL is called a server. A server is a thread that:
Typically, a server handles an exception by resuming its wait for more input. Examples of server threads are event loops and read-eval-print-loops.
thread-become-server! accepts a reset handler and a thread as optional arguments. The thread's interrupt-handler and reset-handler parameters, which by default are per-group, are changed to per-thread. The thread's interrupt handler is set to invoke the reset handler. The thread's reset handler is set to the handler argument if one is supplied, or else is set to issue a warning message and suspend the thread. Finally, the thread is added to the thread group's list of servers.
Exactly one thread in the system is distinguished as the console thread. The console thread is interrupted in response to a Chez Scheme keyboard or Unix process interrupt event. If the optional argument is omitted, the currently running thread becomes the console. thread-become-console! calls thread-become-server!, passing a reset handler that will abort the system if it is called. It is the responsibility of the console thread to install a different reset handler in order to avoid this possibility. This happens automatically if the console thread subsequently starts a new cafe.
thread-break interrupts a thread's normal execution and arranges for an interrupt procedure to be called within the interrupted thread's dynamic execution context. If the interrupt procedure returns normally, the interrupted thread's normal execution will resume. thread-break permits one thread to side-effect another thread's parameter values, or to alter its flow of control.
With no arguments, thread-break interrupts the currently running thread, otherwise the specified thread is interrupted. If reason is supplied, and is not #f, an exception information block is created and stored in the thread-exception (per-thread) and last-exception (global) parameters for the interrupted thread. If thunk is supplied, it is called by the interrupted thread, otherwise that thread's interrupt-handler is called. If run-now? is supplied, the interrupting thread is suspended and the interrupted thread is restarted at highest priority so that the interrupt procedure is run immediately. In this case the interrupting thread is scheduled to resume immediately following the interrupted thread.
thread-debug calls the Chez Scheme debugger. The continuation stored in thread-exception (which belongs to the currently running thread) is inspected, if it exists. Otherwise, the continuation stored in last-exception (which belongs to another thread) is inspected, if it exists. Otherwise a warning message is issued that nothing may be inspected.
A procedure called by the error primitive in response to a thread execution error, which prints error data and thread-identifying information to the console output port.
A thread is a schedulable continuation. thread-fork converts thunk into a new ready-to-run thread in the current thread's group. quantum is the amount of time in milliseconds that the thread may run before it is preempted by the scheduler. If quantum is omitted, it defaults to the value of the parameter thread-default-quantum. priority determines when a thread will be executed when multiple threads are ready to run. A higher priority thread always runs before a lower one. If omitted, priority defaults to the priority of assigned to the caller's thread group.
A thread group is a collection of threads that share a single set of extended parameter group locations. thread-fork-group converts thunk into a new ready-to-run thread in a new group. quantum is the amount time in seconds that the thread may run before it is preempted by the scheduler. If quantum is omitted, it defaults to the value of the parameter thread-default-quantum. priority determines when a thread will be executed when multiple threads are ready to run. A higher priority thread always runs before a lower one. If omitted, priority defaults to the priority of assigned to the caller's thread group. params is a vector of thread group parameter values to be copied into the new group's parameters. If omitted, params defaults to the vector of parameter values assigned to the caller's thread group.
thread-kill terminates the specified thread. num specifies the integer identifier of the thread to be killed. If omitted, the currently running thread is killed.
It is an error to kill the last runnable thread.
A message queue is an object that enables thread synchronization and the exchange of data between cooperating threads. thread-make-msg-queue creates a new empty message queue. name is a datum (typically a Scheme symbol) that denotes the queue.
thread-msg-waiting? returns a #t if one or more messages have been placed on the queue (by a call to thread-send-msg). Otherwise, it returns #f.
If a message is waiting on queue, thread-receive-msg immediately returns the message, otherwise it enqueues the thread on the queue, which blocks it from running until another thread sends a message to the queue.
thread-receiver-waiting? returns #t if a thread is blocked on the queue awaiting a message. Otherwise, it returns #f.
If a receiver thread is waiting on queue, thread-send-msg immediately delivers msg to the thread, causing it to be rescheduled as ready-to-run, otherwise it enqueues the message on the queue and immediately returns. If mode is supplied it must be the symbol urgent. Urgent mode causes the message receiver, if any, to be immediately awakened and run at highest priority. The sending thread yields at highest priority, so that it will immediately follow the receiver when that thread next yields.
thread-reschedule moves previously enqueued thread to a new position within its queue, based on priority, an integer value.
thread-sleep suspends the currently running thread for msecs milliseconds, by placing it on the sleep queue.
thread-wake awakens thread by removing it from the sleep queue and placing it on the run queue.
thread-yield suspends the currently running thread, forcing it to relinquish control to another runnable thread. The current thread's scheduling priority is reset to the priority of its group (see thread-reschedule), and the thread is placed in the run-queue according to its (new) priority.
The return value of thread-yield is used internally by the thread system (by the message queue primitives), and should be ignored by user code.
returns: see explanation
thread-highest-priority returns a negative fixnum value that denotes the highest priority at which a thread may be scheduled to run. (Larger numbers represent lower priorities.)
thread-lowest-priority returns a positive fixnum value that denotes the lowest priority at which a thread may be scheduled to run. (Larger numbers represent lower priorities.)
In standard Chez Scheme a parameter is a procedure that encapsulates a single location. The thread system enhances standard parameters to permit individual threads and thread groups to possess distinct, private locations, or to access a single, global location. A extended parameter is a procedure that manages access to the various assignment locations associated with the parameter.
value and filter are as for standard parameters. value is assigned to the extended parameter's global location.
global variable: thread-timer-interrupt-hook
thread-timer-interrupt-hook is assigned a thunk that is called once for each interrupt taken by the Chez Scheme timer interrupt. Since the thread system triggers the timer interrupt at a very high frequency, it is essential that the timer interrupt hook procedure be made as simple as possible.
thread-run-queue-idle? is #t if one or more threads are ready to run, #f otherwise.
thread-sleep-queue-idle? is #t is one or more threads are sleeping, i.e., blocked on the sleep queue, #f otherwise.
thread-conout is the standard Chez Scheme console output port.
error invokes the thread's error handler. Then, if the thread is not a server, error suspends the thread. Otherwise, error calls reset.
If a currently running thread is not a server, reset issues a warning and returns. Otherwise, it invokes the thread's reset-handler, as in standard Chez Scheme.
new-cafe starts a new Chez Scheme cafe in the currently executing thread group. The thread system supports multiple concurrently executing cafes.
Chez Scheme extends the standard Scheme top-level environment with additional global variables. To provide disciplined access to these system variables, each variable is assigned an interface procedure called a parameter. In standard Chez Scheme, a parameter encapsulates a single location, which stores the value of the parameter.
With multiple threads, parameter locations should often be made private to individual threads, or to thread groups. In the thread system, most Chez Scheme system parameters are reimplemented to provide distinct storage locations for different threads. Parameters with more than one storage location are called extended parameters. In addition to per-thread and per-group locations, each extended parameter retains a single location that is global to all threads. A parameter's location is either private to the thread, or it may be delegated to the thread's group location or to the global location. Location delegation is determined per-thread based on the assignment mode. The default assignment mode uses the group location.
Extended parameters require additional operations and arguments. With zero or one argument, an extended parameter behaves like a standard parameter, except that the accessed location may be local to the thread or group. For clarity, in this section a parameter call with zero or one argument is labeled a standard parameter operation. When called with exactly two arguments, an extended parameter performs an extended operation. The first argument of a two-argument parameter call is the operation and the second argument is the value:
extended parameter: (<parameter> operation value)
returns: see below
For certain extended parameter operations, the value argument is unused, however an argument must still be supplied. The unused argument is a placeholder that provides the number of arguments needed to distinguish extended (two-argument) parameter operations from standard (zero or one argument) ones. The extended operations are as follows:
The thread system adds several standard and extended parameters to the Scheme top-level environment.
returns: see explanation
The thread system replaces the Chez Scheme keyboard-interrupt-handler. Hence, this handler must not be changed by the user.
The thread system replaces the Chez Scheme timer-interrupt-handler. Hence, this handler must not be changed by the user.
parameter: (thread-default-quantum quantum)
returns: an integer
thread-default-quantum is the default execution time quantum in milliseconds that is assigned when a thread is scheduled for execution.
thread-default-ticks is the number of Chez Scheme timer ticks that must elapse before a timer interrupt occurs, thus determining the frequency of timer interrupts.
Higher interrupt frequencies increase both the timing accuracy of the preemptive scheduling algorithm and overhead caused by preemptive scheduling. The default value was selected to give good performance under most conditions.
extended parameter: (cafe-level)
extended parameter: (cafe-level level)
returns: see explanation
SWL enables multiple user interaction environments (read-eval-print loops) to be active concurrently. The Chez Scheme cafe procedure implements one such user environment. In SWL, multiple cafes may be active concurrently, each running in a separate thread group.
cafe-level determines the cafe nesting level of a single Chez Scheme cafe. Hence, one cafe-level parameter location is allocated for each thread group.
interrupt-handler provides each thread with an interrupt thunk.
An executing thread may forcibly alter another thread's normal flow of control by interrupting it via a call to thread-break. thread-break takes an optional argument, an interrupt thunk. If no argument is supplied, the interrupt thunk is taken from the thread's interrupt handler.
When the interrupted thread is next started, control is transferred to the interrupt thunk. If the interrupt thunk returns normally, control is then transferred to the interrupted thread's original continuation.
A thread exception is a data structure created each time a exception occurs during a thread's execution. Kinds of exceptions are errors and interrupts. The exception data structure stores the thread's continuation and other data about the exception. The continuation is available for inspection.
A server is a thread that is designed to handle events or process transactions indefinitely. Kinds of servers are read-eval-print loops and SWL event loops. Under normal circumstances a server thread does not terminate, even when an error exception occurs.
(thread-group-servers) returns a list of all the threads in the current thread's group that are servers.
(thread-name) returns the name assigned to the thread, a Scheme string. The default name is the empty string.
Each thread is assigned an execution priority, a Scheme fixnum integer. The most negative fixnum is the highest priority value. Higher priority threads always execute ahead of lower priority ones.
(thread-quantum) returns the thread's priority.
Each thread is assigned an execution time quantum, specified in seconds. The quantum determines how much CPU time a thread is permitted to consume before it is preempted (i.e., forced to yield in favor of another runnable thread). A typical quantum is ten milliseconds.
(thread-quantum) returns the run-time allotted when the thread is scheduled to run.
A thread context switch does not invoke continuation winders. Hence, the effects of dynamic-wind, fluid-let, and parameterize are not undone when a thread yields control to another thread.
Many Chez Scheme parameters are maintained on a per-thread basis, however, and the values of such threaded parameters are changed with each context switch. Finally, the Chez Scheme interrupt counter, which is affected by the Chez Scheme primitives enable-interrupts, disable-interrupts, and critical-section, is also maintained on a per-thread basis. Thus, for example, a thread yield that occurs within the context of a critical-section effectively cancels the critical-section (and re-enables interrupts) until the invoking thread is restarted.