Chapter 6. The SWL Thread System

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:

The thread system should be optimized for SWL (both its
internals and its application programs).

The presence of multiple threads should be as transparent as
possible to both SWL application developer and end-user. At first
glance, the system should appear to have a single-threaded "look and
feel."

The impact of thread system overhead on single-thread
performance should be minimal.

Compatibility with the Chez Scheme language should be maintained
wherever possible.

the SWL thread system should have minimal dependency on Chez
Scheme system source code and internals.

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.

Section 6.1. API Procedures

The thread system adds a number of user interface procedures to Chez
Scheme.

Section 6.2. Query Primitives

procedure: (ps)procedure: (ps thread)returns: a list of thread data

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.

procedure: (ps-all)returns: see explanation

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.

procedure: (pps)procedure: (pps output-port)returns: unspecified

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).

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:

the name of the queue on which the thread is waiting, and, if
sleeping, the amount of time in seconds before the thread is to be
awakened by the scheduler.

(optional) the keyword priority and an integer value, which
indicates the scheduling priority of the thread (smaller
numbers indicate higher priority), or

(optional) the lists (lowest priority) or
(highest priority), indicating that the thread either
precedes or follows all other threads in scheduling priority.

the running time quantum, in seconds. Indicates the amount of
CPU time the thread will be permitted to consume before it is
preempted by the scheduler.

(optional) the symbol server, if the thread is a server (see
thread-become-server!).

the keyword starts and an integer value, which indicates the
number of times the thread has been executed (i.e., started) by the
scheduler.

procedure: (ps-num)returns: the number of threads in the system

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).

procedure: (thread->k)returns: the thread's continuation

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.

procedure: (thread-number)returns: the thread's id number

thread-number returns the integer identifier of the argument thread,
or the currently executing thread if no argument is supplied.

procedure: (thread-self)returns: the thread itself

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.

procedure: (thread-find)returns: see explanation

thread-find returns the thread corresponding to the integer identifier
argument. If no such thread exists, thread-find returns #f.

Section 6.3. Control Flow Primitives

procedure: (thread-become-server!)returns: unspecified

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:

waits for input from an input source such as the keyboard or
another thread.

When the input is received, the thread awakens and processes the
input.

When the input is fully processed, the thread returns to waiting
for input.

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.

procedure: (thread-debug)returns: unspecified

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.

procedure: (thread-error-handler)returns: unspecified

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.

Section 6.4. Creating and Destroying Threads

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.

procedure: (thread-fork-group)procedure: (thread-fork-group quantum)procedure: (thread-fork-group quantumpriority)procedure: (thread-fork-group quantumpriorityparams)returns: a newly created thread, in a new 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.

Section 6.5. Inter-thread Communication

procedure: (thread-make-msg-queue name)returns: a message queue

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.

procedure: (thread-msg-waiting? queue)returns: a boolean

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.

procedure: (thread-receive-msg queue)returns: a queued message

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.

procedure: (thread-receiver-waiting? queue)returns: a boolean

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.

Section 6.6. Thread Scheduling

procedure: (thread-reschedule threadpriority)returns: unspecified

thread-reschedule moves previously enqueued thread to a new
position within its queue, based on priority, an integer value.

procedure: (thread-sleep msecs)returns: unspecified

thread-sleep suspends the currently running thread for
msecs milliseconds, by placing it on the sleep queue.

procedure: (thread-wake thread)returns: unspecified

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.

Section 6.7. Miscellaneous Primitives

procedure: (thread-highest-priority)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.)

procedure: (thread-lowest-priority)returns: see explanation

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.

Section 6.8. Global Variables

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.

global variable: thread-run-queue-idle?

thread-run-queue-idle? is #t if one or more threads
are ready to run, #f otherwise.

global variable: thread-sleep-queue-idle?

thread-sleep-queue-idle? is #t is one or more
threads are sleeping, i.e., blocked on the sleep queue, #f
otherwise.

global variable: thread-conout

thread-conout is the standard Chez Scheme console output port.

Section 6.9. Redefined Chez Scheme Procedures

procedure: (error)returns: unspecified

error invokes the thread's error handler. Then, if the
thread is not a server, error suspends the thread.
Otherwise, error calls reset.

procedure: (reset)returns: unspecified

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.

procedure: (new-cafe)returns: unspecified

new-cafe starts a new Chez Scheme cafe in the currently
executing thread group. The thread system supports multiple concurrently
executing cafes.

Section 6.10. Enhancement of Chez Scheme Parameters

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> operationvalue)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:

get-assignment-mode, which returns the type of
location, thread (private to a single thread) or
group (shared by all threads of one group), to which a
standard operation applies. Note that the global location cannot be
set by a standard parameter assignment (i.e., there is no "global"
assignment mode). The default assignment mode is group.

set-assignment-mode!, which sets the parameter's
assignment mode. Value must be one of the symbols thread or
group. If thread, the current parameter value
(either group or global) is copied into the new thread location. If
group, the mode must already be set to thread or a
warning will be issued. Otherwise, the thread location is
disabled and group becomes the effective location type.

get-global, returns the global value assigned to
the parameter. value is an unused placeholder.

set-global!, sets the parameter's global location
to value.

get-group, returns the parameter's thread group
value. value is an unused placeholder.

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.

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.

extended parameter: (thread-exception)returns: unspecified

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.

extended parameter: (thread-name)extended parameter: (thread-name name)returns: the name of the thread, a string

(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 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.

Section 6.14. Thread System Effects on Standard Language Constructs

dynamic-wind

fluid-let

parameterize

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.