Chapter 7

Threads

MzScheme supports multiple threads of control within a program. Threads are implemented for all operating systems, even when the operating system does not provide primitive thread support.

(thread thunk) invokes the procedure thunk with no arguments in a new thread of control. The thread procedure returns immediately with a thread descriptor value. When the invocation of thunk returns, the thread created to invoke thunk terminates.

Example:

(thread (lambda () (sleep 2) (display 7) (newline))) ; => a thread descriptor 
 displays 7 after two seconds pass

Each thread has its own parameter settings (see section 7.7), such as the current directory or current exception handler. A newly-created thread inherits the parameter settings of the creating thread, except

When a thread is created, it is placed into the management of the current custodian (See section 9.2).

A thread that has not terminated can be ``garbage collected'' if it is unreachable and suspended, or if it is unreachable and blocked on a set of unreachable waitable objects through semaphore-wait or semaphore-wait/enable-break (see section 7.4), channel-put or channel-get (see section 7.5), object-wait-multiple or object-wait-multiple/enable-break (see section 7.6), or thread-wait.8

7.1  Suspending, Resuming, and Killing Threads

(thread-suspend thread) immediately suspends the execution of thread if it is running. If the thread has terminated or is already suspended, thread-suspend has no effect. The thread remains suspended (i.e., it does not execute) until it is resumed with thread-resume.

(thread-resume thread [yoke-to-thread]) resumes the execution of thread if it is suspended. If the thread has terminated, thread-resume has no effect. Also, if the thread is already running and yoke-to-thread is not supplied, thread-resume has no effect. If yoke-to-thread is supplied, it triggers two additional actions:

(kill-thread thread) terminates the specified thread immediately, or suspends the thread if thread was created with thread/suspend-to-kill. Terminating the main thread exits the application. If thread has already terminated, kill-thread does nothing. Otherwise, if the current custodian (see section 9.2) does not manage thread (and none of its subordinates manages thread), the exn:misc exception is raised.

All of the MzScheme (and MrEd) primitives are kill-safe and suspend-safe; that is, killing or suspending a thread never interferes with the application of primitives in other threads. For example, if a thread is killed while extracting a character from an input port, the character is either completely consumed or not consumed, and other threads can safely use the port.

(thread/suspend-to-kill thunk) is like (thread thunk), except that ``killing'' the current thread through kill-thread or custodian-shutdown-all (see section 9.2) merely suspends the thread instead of terminating it.

7.2  Synchronizing Thread State

(thread-wait thread) blocks execution of the current thread until thread has terminated. Note that (thread-wait (current-thread)) deadlocks the current thread, but a break can end the deadlock (if breaking is enabled; see section 6.6).

(thread-dead-waitable thread) returns a waitable object (see section 7.6) that blocks until thread has terminated. Unlike using thread directly, however, reference to the waitable does not prevent thread from being ``garbage collected.''

(thread-resume-waitable thread) returns a waitable object (see section 7.6) that blocks until thread is running. (If thread has terminated, the waitable will never unblock.) If thread runs and is then suspended after a call to thread-resume-waitable, the result waitable remains unblocked; after each suspend of thread a fresh waitable object is generated to be returned by thread-resume-waitable. The unblock result of the waitable is thread, but if thread is never resumed, then reference to the waitable does not prevent thread from being ``garbage collected.''

(thread-suspend-waitable thread) returns a waitable object (see section 7.6) that blocks until thread is suspended. (If thread has terminated, the waitable will never unblock.) If thread is suspended and the resumes after a call to thread-suspend-waitable, the result waitable remains unblocked; after each resume of thread a fresh waitable object is generated to be returned by thread-suspend-waitable.

7.3  Additional Thread Utilities

(current-thread) returns the thread descriptor for the currently executing thread.

(thread? v) returns #t if v is a thread descriptor, #f otherwise.

(sleep [x]) causes the current thread to sleep for at least x seconds, where x is a non-negative real number. The x argument defaults to 0 (allowing other threads to execute when operating system threads are not used). The value of x can be non-integral to request a sleep duration to any precision, but the precision of the actual sleep time is unspecified.

(thread-running? thread) returns #t if thread has not terminated and is not suspended, #f otherwise.

(thread-dead? thread) returns #t if thread has terminated, #f otherwise.

(break-thread thread) registers a break with the specified thread. If breaking is disabled in thread, the break will be ignored until breaks are re-enabled (see section 6.6).

(call-in-nested-thread thunk [custodian]) creates a nested thread managed by custodian to execute thunk.9 The current thread blocks until thunk returns, and the result of the call-in-nested-thread call is the result returned by thunk. The default value of custodian is the current custodian (see section 9.2).

The nested thread's exception handler is initialized to a procedure that jumps to the beginning of the thread and transfers the exception to the original thread. The handler thus terminates the nested thread and re-raises the exception in the original thread.

If the thread created by call-in-nested-thread dies before thunk returns, the exn:thread exception is raised in the original thread. If the original thread is killed before thunk returns, a break is queued for the nested thread.

If a break is queued for the original thread (with break-thread) while the nested thread is running, the break is redirected to the nested thread. If a break is already queued on the original thread when the nested thread is created, the break is moved to the nested thread. If a break remains queued on the nested thread when it completes, the break is moved to the original thread.

7.4  Semaphores

A semaphore is a value that is used to synchronize MzScheme threads. Each semaphore has an internal counter; when this counter is zero, the semaphore can block a thread's execution (through semaphore-wait) until another thread increments the counter (using semaphore-post). The maximum value for a semaphore's internal counter is platform-specific, but always at least 10000.

A semaphore's counter is updated in a single-threaded manner, so that semaphores can be used for reliable synchronization. Semaphore waiting is fair: if a thread is blocked on a semaphore and the semaphore's internal value is non-zero arbitrarily often, then the thread is eventually unblocked.

See also object-wait-multiple in section 7.6.

7.5  Channels

A synchronous channel is a value that is used to synchronize MzScheme threads with a one-way rendevous: one thread sends a value to another thread, and both the sender and the receiver block until the (atomic) transaction is complete. Multiple senders and receivers can access a channel at once, but a single sender and receiver is selected for each transaction.

Channel waiting is fair: if a thread is blocked on a channel and transaction opportunities for the channel occur arbitrarily often, then the thread eventually participates in a transaction.

For buffered asynchronous channels, see Chapter mz:mzlibasync-channel in PLT MzLib: Libraries Manual.

7.6  Synchronizing Multiple Objects with Timeout

(object-wait-multiple timeout waitable ···1) blocks as long as all of the waitables are in a blocking state, as defined below, or until timeout seconds have passed. The timeout argument can be a real number or #f; if timeout is #f, then object-wait-multiple does not return until some waitable is unblocked.

When at least one waitable is unblocked, its unblock result (often waitable itself) is returned. If multiple waitables are unblocked at once, one of the waitables is chosen pseudo-randomly for the result. (The current-wait-pseudo-random-generator parameter sets the random-number generator that controls this choice; see section 7.7.1.10.)

Choosing an unblocked waitable may affect the state of waitable. For example, if the chosen unblocked waitable is a semaphore, then the semaphore's internal count is decremented, just as with semaphore-wait. For any other built-in kind of waitable (such as a port), waitable's state is not modified.

If object-wait-multiple returns because timeout seconds have passed, the return value is #f. If the value of timeout is 0, each waitable is checked at least once, so a timeout value of 0 can be used for polling.

Only certain kinds of values, listed below, are waitable in stand-alone MzScheme. If any other kind of value is provided to object-wait-multiple, the exn:application:type exception is raised. An extension or embedding application can extend the set of waitable values. In particular, an eventspace in MrEd is waitable.

(object-wait-multiple/enable-break timeout waitable ···1) is like object-wait-multiple, but breaking is enabled (see section 6.6) while waiting on the waitables. If breaking is disabled when object-wait-multiple/enable-break is called, then either all waitables remain unchosen or the exn:break exception is raised, but not both.

(waitables->waitable-set waitable ···) creates and returns a single waitable value that combines the waitable waitables. Supplying the result to object-wait-multiple is the same as supplying each waitable to the same call.

(make-wrapped-waitable waitable convert-proc) creates a waitable that is in a blocking state when waitable is in a blocking state, but whose unblock result is determined by applying convert-proc to the unblock result of waitable.

(make-guard-waitable generator-thunk) creates a value that behaves as a waitable, but that is actually a waitable generator. For details, see object-wait-multiple, above.

(make-nack-guard-waitable generator-proc) creates a value that behaves as a waitable, but that is actually a waitable generator; the generator procedure receives a waitable that becomes ready with a void value if the generated waitable was not ultimately chosen. For details, see object-wait-multiple, above.

(make-poll-guard-waitable generator-proc) creates a value that behaves as a waitable, but that is actually a waitable generator; the generator procedure receives a boolean indicating whether the waitable is used for polling. For details, see object-wait-multiple, above.

(object-waitable? v) returns #t if v is a waitable object, #f otherwise. See object-wait-multiple, above, for the list of waitable object types.

7.7  Parameters

A parameter is a thread-specific setting, such a the current output port or the current directory for resolving relative pathnames. A parameter procedure sets and retrieves the value of a specific parameter. For example, the current-output-port parameter procedure sets and retrieves a port value that is used by display when a specific output port is not provided. Applying a parameter procedure without an argument obtains the current value of a parameter in the current thread, and applying a parameter procedure to a single argument sets the parameter's value in the current thread (and returns void). For example, (current-output-port) returns the current default output port, while (current-output-port p) sets the default output port to p.

7.7.1  Built-in Parameters

MzScheme's built-in parameter procedures are listed in the following sections. The make-parameter procedure, described in section 7.7.2, creates a new parameter and returns a corresponding parameter procedure.

7.7.1.1  Current Directory

7.7.1.2  Ports

7.7.1.3  Parsing

7.7.1.4  Printing

7.7.1.5  Read-Eval-Print

7.7.1.6  Loading

7.7.1.7  Exceptions

7.7.1.8  Security

7.7.1.9  Exiting

7.7.1.10  Random Numbers

7.7.1.11  Locale

7.7.1.12  Modules

7.7.2  Parameter Utilities

(make-parameter v [guard-proc]) returns a new parameter procedure. The value of the parameter is initialized to v in all threads. If guard-proc is supplied, it is used as the parameter's guard procedure. A guard procedure takes one argument. Whenever the parameter procedure is applied to an argument, the argument is passed on to the guard procedure. The result returned by the guard procedure is used as the new parameter value. A guard procedure can raise an exception to reject a change to the parameter's value.

(parameter? v) returns #t if v is a parameter procedure, #f otherwise.

(parameter-procedure=? a b) returns #t if the parameter procedures a and b always modify the same parameter, #f otherwise.

The parameterize form evaluates an expression with temporarily values installed for a group of parameters. The syntax of parameterize is:

(parameterize ((parameter-expr value-expr) ···) body-expr ···1)

The result of a parameterize expression is the result of the last body-expr. The parameter-exprs determine the parameters to set, and the value-exprs determine the corresponding values to install before evaluating the body-exprs. All of the parameter-exprs are evaluated first (checked with check-parameter-procedure), then all value-exprs are evaluated, and then the parameters are set.

After the body-exprs are evaluated, each parameter's setting is restored to its original value in the dynamic context of the parameterize expression. More generally, the values specified by the value-exprs determine initial ``remembered'' values, and whenever control jumps into or out of the body-exprs, the value of each parameter is swapped with the corresponding ``remembered'' value.

Examples:

(parameterize ([exit-handler (lambda (x) 'no-exit)]) 
  (exit)) ; => 'no-exit

(define p1 (make-parameter 1))
(define p2 (make-parameter 2))
(parameterize ([p1 3]
               [p2 (p1)]) 
  (cons (p1) (p2))) ; => '(3 . 1)

(let ([k (let/cc out 
           (parameterize ([p1 2]) 
             (p1 3) 
             (cons (let/cc k 
                     (out k)) 
                   (p1))))]) 
  (if (procedure? k) 
      (k (p1))
      k)) ; =>  '(1 . 3)

(check-parameter-procedure v) returns v if it is a procedure that can take both 0 arguments and 1 argument, and raises exn:application:type otherwise. The check-parameter-procedure procedure is used in the expansion of parameterize.


8 In MrEd, a handler thread for an eventspace is blocked on an internal semaphore when its event queue is empty. Thus, the handler thread is collectable when the eventspace is unreachable and contains no visible windows or running timers.

9 The nested thread's current custodian is inherited from the creating thread, independent of the custodian argument.

10 Using the current global port print handler; see section 7.7.1.2.

11 The "C" locale is also always available; setting the locale to "C" is the same as disabling locale sensitivity with #f only when string and character operations are restricted to the first 128 characters.