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.9), 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) and added to the current thread group (see section 9.3). A thread can have any number of custodian managers added through thread-resume.

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 events through semaphore-wait or semaphore-wait/enable-break (see section 7.4), channel-put or channel-get (see section 7.5), sync or sync/enable-break (see section 7.7), or thread-wait.16

All constant-time procedures and operations provided by MzScheme are thread-safe because they are atomic. For example, set! assigns to a variable as an atomic action with respect to all threads, so that no thread can see a ``half-assigned'' variable. Similarly, vector-set! assigns to a vector atomically. The hash-table-put! procedure is not atomic, but the table is protected by a lock; see section 3.14 for more information. Port operations are generally not atomic, but they are thread-safe in the sense that a byte consumed by one thread from an input port will not be returned also to another thread, and procedures like port-commit-peeked (see section 11.2.1) and write-bytes-avail (see section 11.2.2) offer specific concurrency guarantees.

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. If the current custodian (see section 9.2) does not manage thread (and none of its subordinates manages thread), the exn:fail:contract exception is raised, and the thread is not suspended.

(thread-resume thread [thread-or-custodian]) resumes the execution of thread if it is suspended and has at least one custodian (possibly added through thread-or-custodian, as described below). If the thread has terminated, or if the thread is already running and thread-or-custodian is not supplied, or if the thread has no custodian and thread-or-custodian is not supplied, then thread-resume has no effect. Otherwise, if thread-or-custodian is supplied, it triggers up to three 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. If the current custodian (see section 9.2) does not manage thread (and none of its subordinates manages thread), the exn:fail:contract exception is raised, and the thread is not killed or suspended.

Unless otherwise noted, procedures provided by MzScheme (and MrEd) are kill-safe and suspend-safe; that is, killing or suspending a thread never interferes with the application of procedures 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.7).

(thread-dead-evt thread) returns a synchronizable event (see section 7.7) that is ready if and only if thread has terminated. Unlike using thread directly, however, a reference to the event does not prevent thread from being ``garbage collected.''

(thread-resume-evt thread) returns a synchronizable event (see section 7.7) that becomes ready when thread is running. (If thread has terminated, the event never becomes ready.) If thread runs and is then suspended after a call to thread-resume-evt, the result event remains ready; after each suspend of thread a fresh event is generated to be returned by thread-resume-evt. The result of the event is thread, but if thread is never resumed, then reference to the event does not prevent thread from being ``garbage collected.''

(thread-suspend-evt thread) returns a synchronizable event (see section 7.7) that becomes ready when thread is suspended. (If thread has terminated, the event will never unblock.) If thread is suspended and then resumes after a call to thread-suspend-evt, the result event remains ready; after each resume of thread created a fresh event to be returned by thread-suspend-evt.

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

(call-in-nested-thread thunk [custodian]) creates a nested thread managed by custodian to execute thunk.17 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:fail 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 infinitely often, then the thread is eventually unblocked.

See also sync in section 7.7.

7.5  Channels

A synchronous channel is a value that is used to synchronize MzScheme threads: 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 synchronization is fair: if a thread is blocked on a channel and transaction opportunities for the channel occur infinitely often, then the thread eventually participates in a transaction.

For buffered asynchronous channels, see Chapter 2 in PLT MzLib: Libraries Manual.

7.6  Alarms

An alarm is a synchronizable event (see section 7.7) that is ready only after particular date and time. The time is specified as a real number that is consistent with current-inexact-milliseconds (see section 15.1.2).

(alarm-evt msecs-n) returns a synchronizable event for use with sync. The event is not ready when (current-inexact-milliseconds) would return a value that is less than msecs-n, and it is ready when (current-inexact-milliseconds) would return a value that is more than msecs-n.

The sync function accepts a timeout argument in addition to alarm events. Unlike the timeout, however, the result of alarm-evt can be combined with wrap-evt and other event operations.

7.7  Synchronizing Events

(sync evt ···1) blocks as long as none of the synchronizable events evts are ready, as defined below. Certain kinds of objects double as events, including ports and threads, and other kinds of objects exist only for their use as events.

(sync/timeout timeout evt ···1) is like sync, but with a timeout. If no evt is ready before timeout seconds have passed, the result is #f. The timeout argument can be a real number or #f; if timeout is #f, then sync/timeout behaves like sync. If timeout is 0, each evt is checked at least once, so a timeout value of 0 can be used for polling. (See alarm-evt in section 7.6 for an alternative timeout mechanism.)

For either sync or sync/timeout, when at least one evt is ready, its result (often evt itself) is returned. If multiple evts are ready, one of the evts is chosen pseudo-randomly for the result. (The current-evt-pseudo-random-generator parameter sets the random-number generator that controls this choice; see section 7.9.1.10.)

Choosing a ready evt may affect the state of evt. For example, if the chosen ready evt is a semaphore, then the semaphore's internal count is decremented, just as with semaphore-wait. For most kinds of events, however (such as a port), evt's state is not modified.

Only certain kinds of built-in values, listed below, act as events in stand-alone MzScheme. If any other kind of value is provided to sync, the exn:fail:contract exception is raised. An extension or embedding application can extend the set of primitive events -- in particular, an eventspace in MrEd is an event -- and new structure types can generate events (see section 4.7).

(sync/enable-break evt ···1) is like sync, but breaking is enabled (see section 6.7) while waiting on the evts. If breaking is disabled when sync/enable-break is called, then either all evts remain unchosen or the exn:break exception is raised, but not both.

(sync/timeout/enable-break timeout evt ···1) is like sync/enable-break, but with a timeout in seconds (or #f, as for sync/timeout).

(choice-evt evt ···) creates and returns a single event that combines the evts. Supplying the result to sync is the same as supplying each evt to the same call.

(wrap-evt evt wrap-proc) creates an event that is in a ready when evt is ready, but whose result is determined by applying wrap-proc to the result of evt. The call to wrap-proc is parameterize-breaked to disable breaks initially. The evt cannot be an event created by handle-evt or any combination of choice-evt involving an event from handle-evt.

(handle-evt evt handle-proc) is like wrap-evt, except that handle-proc is called in tail position with respect to the synchronization request, and without breaks explicitly disabled.

(guard-evt generator-thunk) creates a value that behaves as an event, but that is actually an event generator. For details, see sync, above.

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

(poll-guard-evt generator-proc) creates a value that behaves as an event, but that is actually an event generator; the generator procedure receives a boolean indicating whether the event is used for polling. For details, see sync, above.

always-evt is a global constant event that is always ready, with itself as its result.

never-evt is a global constant event that is never ready.

(evt? v) returns #t if v is a synchronizable event, #f otherwise. See sync, above, for the list of built-in types that act as synchronizable events.

(handle-evt? evt) returns #t if evt was created by handle-evt or by choice-evt applied to another event for which handle-evt? produces #t. Such events are illegal as an argument to handle-evt or wrap-evt, because they cannot be wrapped further. For any other event, handle-evt? produces #f, and the event is a legal argument to handle-evt or wrap-evt for further wrapping.

7.8  Thread-Local Storage Cells

A thread cell contains a thread-specific value; that is, it contains a specific value for each thread, but it may contain different values for different threads. A thread cell is created with a default value that is used for all existing threads. When the cell's content is changed with thread-cell-set!, the cell's value changes only for the current thread. Similarly, thread-cell-ref obtains the value of the cell that is specific to the current thread.

A thread cell's value can be preserved, which means that when a new thread is created, the cell's initial value for the new thread is the same as the creating thread's current value. If a thread cell is non-preserved, then the cell's initial value for a newly created thread is the default value (which was supplied when the cell was created).

Within the current thread, the current values of all preserved threads cells can be captured through current-preserved-thread-cell-values. The captured set of values can be imperatively installed into the current thread through another call to current-preserved-thread-cell-values. The capturing and restoring threads can be different.

Examples:

(define cnp (make-thread-cell '(nerve) #f))
(define cp (make-thread-cell '(cancer) #t))

(thread-cell-ref cnp) ; => '(nerve)
(thread-cell-ref cp) ; => '(cancer)

(thread-cell-set! cnp '(nerve nerve))
(thread-cell-set! cp '(cancer cancer))

(thread-cell-ref cnp) ; => '(nerve nerve)
(thread-cell-ref cp) ; => '(cancer cancer)

(define ch (make-channel))
(thread (lambda ()
          (channel-put ch (thread-cell-ref cnp))
          (channel-put ch (thread-cell-ref cp))
          (channel-get ch) ; to wait
          (channel-put ch (thread-cell-ref cp))))


(channel-get ch) ; => '(nerve)
(channel-get ch) ; => '(cancer cancer)

(thread-cell-set! cp '(cancer cancer cancer))

(thread-cell-ref cp) ; => '(cancer cancer cancer)
(channel-put ch 'ok)
(channel-get ch) ; => '(cancer cancer)

7.9  Parameters

A parameter is a setting that is both thread-specific and continuation-specific, such as the current output port or the current directory for resolving relative file paths. A parameter procedure retrieves and sets 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 continuation, and applying a parameter procedure to a single argument sets the parameter's value in the current thread and continuation (returning void). For example, (current-output-port) returns the current default output port, while (current-output-port p) sets the default output port to p.

In the empty continuation, each parameter corresponds to a preserved thread cell (see section 7.8); the parameter procedure accesses and sets the thread cell's value (for the current thread). To parameterize code in a continuation-friendly manner, use parameterize. The parameterize form introduces a fresh thread cell for the dynamic extent of its body expressions. 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 while evaluating the body-exprs. All of the parameter-exprs are evaluated first (and checked with parameter?), then all value-exprs are evaluated, and then the parameters are bound in the continuation to preserved thread cells that contain the values of the value-exprs. The last body-expr is in tail position with respect to the entire parameterize form.

Outside the dynamic extent of a parameterize expression, parameters remain bound to other thread cells. Effectively, therefore, old parameters settings are restored as control exits the parameterize expression.

If a continuation is captured during the evaluation of parameterize, invoking the continuation effectively re-introduces the parameterization. More generally, a continuation's parameter-to-thread-cell mapping is called a parameterization, and a parameterization is associated to a continuation via a continuation mark (see section 6.6) using a private key. The current-parameterization procedure returns the current continuation's parameterization. The call-with-parameterization procedure takes a parameterization and a thunk; it sets the current continuation's parameterization to the given one, and calls the thunk through a tail call.

When a new thread is created, the parameterization for the new thread's initial continuation is the parameterization of the creator thread. Since each parameter's thread cell is preserved, the new thread ``inherits'' the parameter values of its creating thread. When a continuation is moved from one thread to another, settings introduced with parameterize effectively move with the continuation. In contrast, direct assignment to a parameter (by calling the parameter procedure with a value) changes the value in a thread cell, and therefore changes the setting only for the current thread. (Consequently, as far as the memory manager is concerned, the value originally associated with a parameter through parameterize remains reachable as long the continuaton is reachable, even if the parameter is mutated.)

Examples:

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

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

(define ch (make-channel))
(parameterize ([p1 0])
  (thread (lambda ()
            (channel-put ch (cons (p1) (p2))))))
(channel-get ch)  ; => '(0 . 2)

(define k-ch (make-channel))
(define (send-k)
  (parameterize ([p1 0])
    (thread (lambda ()
              (let/ec esc
                (channel-put ch
                             ((let/cc k
                                (channel-put k-ch k)
                                (esc)))))))))
(send-k)
(thread (lambda () ((channel-get k-ch) (let ([v (p1)]) (lambda () v)))))
(channel-get ch) ; => 1
(send-k)
(thread (lambda () ((channel-get k-ch) p1)))
(channel-get ch) ; => 0

MzScheme parameters correspond to preserved thread fluids in Scsh. See also ``Processes vs. User-Level Threads in Scsh'' by Gasbichler and Sperber (proceedings of the 2002 Scheme Workshop).

7.9.1  Built-in Parameters

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

7.9.1.1  Current Directory

7.9.1.2  Ports

7.9.1.3  Parsing

7.9.1.4  Printing

7.9.1.5  Read-Eval-Print

7.9.1.6  Loading

7.9.1.7  Exceptions

7.9.1.8  Security

7.9.1.9  Exiting

7.9.1.10  Random Numbers

7.9.1.11  Locale

7.9.1.12  Modules

7.9.1.13  Performance Tuning

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

(current-parameterization) returns the current continuation's parameterization.

(call-with-parameterization parameterization thunk) calls thunk (via a tail call) with parameterization as the current parameterization.

(parameterization? v) returns #t if v is a parameterization returned by current-parameterization, #f otherwise.


16 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 collectible when the eventspace is unreachable and contains no visible windows or running timers.

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

18 The default error display handler in DrScheme also uses the second argument to highlight source locations.

19 Using the current global port print handler; see section 7.9.1.2.

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