Chapter 14. Thread System

This chapter describes the Chez Scheme thread-system procedures and syntactic forms. The thread system is implemented on top of the Posix thread system (pthreads). Consult pthreads documentation on your system for basic details of thread creation and interaction.

Most primitive Scheme procedures are thread-safe, meaning that they can be called concurrently from multiple threads. This includes allocation operations like cons and make-string, accessors like car and vector-ref, numeric operators like + and sqrt, and nondestructive higher-level primitive operators like append and map. Destructive operators are also thread safe if they are used to operate on different objects; e.g., putprop can be called concurrently in two threads if the symbols whose property lists are being modified are different. The last comment applies as well to I/O operations; see also Section 14.6 for information on buffered ports. Any hashtable procedure passed an eq or eqv hashtable may need to rehash the table after a garbage collection occurs and so must be considered destructive operations in this context (Section 7.11).

The compiler and interpreter are also thread-safe, so two or more threads can call any of the compiler or interpreter entry points, i.e., compile, compile-file, compile-script, compile-port, or interpret at the same time. The same is true for eval and load as long as the default evaluator is used or is set explicitly to compile, interpret, or some other thread-safe evaluator.

One restriction should be observed when one of multiple threads creates or loads compiled code, however, which is that only that thread or subsequently created children, or children of subsequently created children, etc., should run the code. This is because multiple-processor systems upon which threaded code may run might not guarantee that the data and instruction caches are synchronized across processors.

Section 14.1. Thread Creation


procedure: (fork-thread thunk)
returns: a thread object

thunk must be a procedure that accepts zero arguments.

fork-thread Invokes thunk in a new thread and returns a thread object.

At present, nothing can be done with the thread object returned by fork-thread, other than to print it. In the future, it may be possible to signal or kill threads using the thread object as a handle on the thread.

Threads created by foreign code using some means other than fork-thread must call Sactivate_thread (Section 4.6) before touching any Scheme data or calling any Scheme procedures.


procedure: (thread? obj)
returns: #t if obj is a thread object, #f otherwise

Section 14.2. Mutexes


procedure: (make-mutex)
returns: a new mutex object


procedure: (mutex? obj)
returns: #t if obj is a mutex, #f otherwise


procedure: (mutex-acquire mutex)
procedure: (mutex-acquire mutex block?)
returns: see below

mutex must be a mutex.

mutex-acquire acquires the mutex identified by mutex. The optional boolean argument block? defaults to #t and specifies whether the thread should block waiting for the mutex. If block? is omitted or is true, the thread blocks until the mutex has been acquired, and an unspecified value is returned.

If block? is false and the mutex currently belongs to a different thread, the current thread does not block. Instead, mutex-acquire returns immediately with the value #f to indicate that the mutex is not available. If block? is false and the mutex is successfully acquired, mutex-acquire returns #t.

Mutexes are recursive in Posix threads terminology, which means that the calling thread can use mutex-acquire to (re)acquire a mutex it already has. In this case, an equal number of mutex-release calls is necessary to release the mutex.


procedure: (mutex-release mutex)
returns: unspecified

mutex must be a mutex.

mutex-release releases the mutex identified by mutex. Unpredictable behavior results if the mutex is not owned by the calling thread.


syntax: (with-mutex mutex exp1 exp2 ...)
returns: the value of the final expression

with-mutex evaluates the expression mutex, which must evaluate to a mutex, acquires the mutex, evaluates the body exp1 exp2 ..., and releases the mutex. The mutex is released whether the body returns normally or via a control operation (that is, throw to a continuation, perhaps because of an error) that results in a nonlocal exit from the with-mutex form. If control subsequently returns to the body via a continuation invocation, the mutex is reacquired.

Using with-mutex is generally more convenient and safer than using mutex-acquire and mutex-release directly.

Section 14.3. Conditions


procedure: (make-condition)
returns: a new condition object


procedure: (condition? obj)
returns: #t if obj is a condition object, #f otherwise


procedure: (condition-wait cond mutex)
returns: unspecified

cond must be a condition object, and mutex must be a mutex.

condition-wait waits for the condition identified by the condition object cond. The calling thread must have acquired the mutex identified by the mutex mutex at the time condition-wait is called. mutex is released as a side effect of the call to condition-wait. When a thread is later released from the condition variable by one of the procedures described below, mutex is reacquired and condition-wait returns.


procedure: (condition-signal cond)
returns: unspecified

cond must be a condition object.

condition-signal releases one of the threads waiting for the condition identified by cond.


procedure: (condition-broadcast cond)
returns: unspecified

cond must be a condition object.

condition-broadcast releases all of the threads waiting for the condition identified by cond.

Section 14.4. Thread Parameters


procedure: (make-thread-parameter object)
procedure: (make-thread-parameter object procedure)
returns: a new thread parameter

See Section 11.10 for a general discussion of parameters and the use of the optional second argument.

When a thread parameter is created, a separate location is set aside in each current and future thread to hold the value of the parameter's internal state variable. (This location may be eliminated by the storage manager when the parameter becomes inaccessible.) Changes to the thread parameter in one thread are not seen by any other thread.

When a new thread is created (see fork-thread), the current value (not location) of each thread parameter is inherited from the forking thread by the new thread. Similarly, when a thread created by some other means is activated for the first time (see Sactivate_thread in Section 4.6), the current value (not location) of each thread parameter is inherited from the main (original) thread by the new thread.

Section 14.5. Built-in Parameters

Most built-in parameters are thread parameters, but some are global. Here is a list of the built-in thread parameters.

abort-handler
break-handler
case-sensitive
compile-compressed
compile-interpret-simple
compile-profile
cp0-effort-limit
cp0-polyvariant
cp0-score-limit
current-eval
current-expand
current-input-port
current-output-port
error-handler
eval-syntax-expanders-when
exit-handler
generate-inspector-information
generate-interrupt-trap
interaction-environment
internal-defines-as-letrec*
keyboard-interrupt-handler
optimize-level
pretty-initial-indent
pretty-line-length
pretty-maximum-lines
pretty-one-line-limit
pretty-standard-indent
print-brackets
print-gensym
print-graph
print-length
print-level
print-radix
print-record
print-vector-length
random-seed
reset-handler
run-cp0
subset-mode
timer-interrupt-handler
trace-output-port
trace-print
waiter-prompt-and-read
waiter-prompt-string
waiter-write
warning-handler

The initial values of most of these are simply copied from the forking thread. Random seed is reset to its initial value when a thread is created. Three other exceptions are reset-handler, exit-handler, and abort-handler. Each is bound to a procedure that exits immediately from the thread. One effect of this is that errors in all but the main thread cause the thread to exit after signaling an error.

The remaining, global, parameters are listed below.

collect-generation-radix
collect-notify
collect-request-handler
collect-trip-bytes
command-line
command-line-arguments
console-input-port
console-output-port
current-directory (cd)
gensym-count
gensym-prefix
scheme-script
scheme-start
source-directories
suppress-greeting

Section 14.6. Buffered I/O

Chez Scheme buffers file I/O operations for efficiency, but buffered I/O is not thread safe. Two threads that write to or read from the same buffered port concurrently can corrupt the port, resulting in buffer overruns and, ultimately, invalid memory references.

Both input and output ports are normally buffered by default: When running the threaded version of Chez Scheme, however, the console output port is unbuffered by default. This allows multiple threads to print error and/or debugging messages to the console. The output may be interleaved, even within the same line, but the port will not become corrupted.

Threads that wish to allow multiple threads to write to a file via a single port should use the unbuffered flag when opening the file, e.g.:

(define p (open-output-file "prog.out" '(unbuffered)))

Although open-input-file also accepts the unbuffered flag, "unbuffered" input ports are not entirely unbuffered because at least one character of buffering is required to support peek-char and unread-char. Therefore, two threads should never read from the same input port concurrently.

Section 14.7. Example

The following code implements a bounded queue using many of the thread-system features. A bounded queue has a fixed number of available slots. Attempting to enqueue when the queue is full causes the calling thread to block. Attempting to dequeue from an empty queue causes the calling thread to block.

(define-record bq (i)
  ([vec (make-vector i)]
   [mutex (make-mutex)]
   [ready (make-condition)]
   [room (make-condition)]))

(define enqueue!
  (lambda (bq item)
    (let ([mutex (bq-mutex bq)])
      (with-mutex mutex
        (let loop ()
          (when (zero? (bq-i bq))
            (condition-wait (bq-room bq) mutex)
           ; we reacquire the mutex when we wake up, but some other
           ; thread may beat us to the punch
            (loop)))
        (let ([i (- (bq-i bq) 1)])
          (vector-set! (bq-vec bq) i item)
          (set-bq-i! bq i)
          (condition-signal (bq-ready bq)))))))

(define dequeue!
  (lambda (bq)
    (let ([mutex (bq-mutex bq)])
      (with-mutex mutex
        (let loop ()
          (when (= (bq-i bq) (vector-length (bq-vec bq)))
            (condition-wait (bq-ready bq) mutex)
           ; we reacquire the mutex when we wake up, but some other
           ; thread may beat us to the punch
            (loop)))
        (let ([i (bq-i bq)])
          (let ([item (vector-ref (bq-vec bq) i)])
            (set-bq-i! bq (+ i 1))
            (condition-signal (bq-room bq))
            item))))))

The code below demonstrates the use of the bounded queue abstraction with a set of threads that act as consumers and producers of the data in the queue.

(define job-queue)

(define make-job
  (let ([count 0])
    (lambda (n)
      (define fib
        (lambda (n) (if (< n 2) n (+ (fib (- n 2)) (fib (- n 1))))))
      (set! count (+ count 1))
      (printf "Adding job #~s = (lambda () (fib ~s))~n" count n)
      (cons count (lambda () (fib n))))))

(define producer
  (lambda ()
    (enqueue! job-queue (make-job (+ 25 (random 10))))
    (producer)))

(define make-consumer
  (lambda (n)
    (rec consumer
      (lambda ()
        (printf "consumer ~s looking for a job~%" n)
        (let ([job (dequeue! job-queue)])
          (printf "consumer ~s executing job #~s~%" n (car job))
          (printf "consumer ~s computed:  ~s~%" n ((cdr job))))
        (consumer)))))

(define (bq-test n)
  (set! job-queue (make-bq n))
  (fork-thread producer)
  (do ([n n (- n 1)])
      ((<= n 0))
      (fork-thread (make-consumer n))))

Here are a few lines of output from a sample run of the example program.

> (bq-test 5)
Adding job #1 = (lambda () (fib 34))
Adding job #2 = (lambda () (fib 31))
Adding job #3 = (lambda () (fib 27))
Adding job #4 = (lambda () (fib 26))
Adding job #5 = (lambda () (fib 29))
Adding job #6 = (lambda () (fib 32))
consumer 5 looking for a job
Adding job #7 = (lambda () (fib 31))
consumer 5 executing job #5
consumer 4 looking for a job
Adding job #8 = (lambda () (fib 29))
consumer 4 executing job #6
consumer 3 looking for a job
Adding job #9 = (lambda () (fib 30))
consumer 3 executing job #7
consumer 2 looking for a job
Adding job #10 = (lambda () (fib 25))
consumer 2 executing job #8
consumer 5 computed:  514229
consumer 5 looking for a job
Adding job #11 = (lambda () (fib 33))
consumer 5 executing job #9
consumer 1 looking for a job
Adding job #12 = (lambda () (fib 34))
consumer 1 executing job #10
consumer 1 computed:  75025
consumer 1 looking for a job
Adding job #13 = (lambda () (fib 31))
consumer 1 executing job #11
...

R. Kent Dybvig / Chez Scheme Version 7 User's Guide
Copyright © 2005 R. Kent Dybvig
Revised July 2007 for Chez Scheme Version 7.4
Cadence Research Systems / www.scheme.com
Cover illustration © 1998 Jean-Pierre Hébert
ISBN: 0-9667139-1-5
to order this book / about this book