Racket has four ways to do several things at once, which differ in whether they share memory and whether they run in parallel: threads (concurrent, sharing memory), parallel threads (parallel, sharing memory), futures (parallel, restricted to operations that are safe in parallel) and places (parallel, with separate memory and message passing). All of them are in racket/base or racket libraries; none needs a package.
Threads
(thread thunk) runs thunk in a new thread of control and returns a thread descriptor at once. By default a thread is a coroutine thread: Racket runs all of them on one operating-system thread and switches among them, so they are concurrent but not parallel (and the scheduler may switch between threads at almost any point, so shared mutable data still needs care). Two keyword arguments were added in version 8.18.0.2:
#:pool pool–#f(the default) for a coroutine thread;'ownto start a parallel thread in a new pool that is then closed to other threads; or a pool made bymake-parallel-thread-pool, so that the new thread runs in parallel with others in other pools. Parallel threads do not run in parallel on the BC variant of Racket, or when Racket is built without support for parallelism.#:keep 'results– records what the thunk returns, to be given bythread-wait.
| Function | Does |
|---|---|
thread?, current-thread | test for a thread; the running thread |
thread-wait, thread-dead-evt, thread-running?, thread-dead? | wait for a thread to end and (if it kept them) get its results; an event ready when it ends; state tests |
kill-thread, break-thread, thread-suspend, thread-resume, thread/suspend-to-kill | stop a thread; send it a break (like Ctrl-C); pause and continue it; make one that can only be suspended when its custodian ends |
sleep, yield | pause for some seconds, or let others run |
thread-send, thread-receive, thread-try-receive, thread-receive-evt, thread-rewind-receive | each thread has a mailbox: send a value to a thread, wait for one, or poll |
make-thread-cell, thread-cell-ref, thread-cell-set! | per-thread storage |
call-in-nested-thread | runs a thunk in a thread and waits for the result |
make-parallel-thread-pool, parallel-thread-pool?, parallel-thread-pool-close | pools for parallel threads; the size defaults to (processor-count) |
Synchronisation and events
A thread waits on an event. A semaphore, a channel, a thread, a port, a timer, a TCP listener and many other values can be used as events, and (sync evt …) blocks until one is ready and returns its value. (sync/timeout secs evt …) gives up after a time and returns #f; sync/enable-break allows breaks while it waits.
| Item | Is |
|---|---|
(make-semaphore n), semaphore-post, semaphore-wait, semaphore-try-wait?, semaphore-peek-evt, call-with-semaphore | a counter with a waiting queue: post adds one, wait takes one or blocks |
(make-channel), channel-put, channel-get, channel-try-get, channel-put-evt | a synchronous rendezvous: a put and a get meet |
make-async-channel, async-channel-put, async-channel-get (racket/async-channel) | a buffered channel |
choice-evt, wrap-evt, handle-evt, guard-evt, nack-guard-evt, poll-guard-evt, replace-evt, never-evt, always-evt, alarm-evt, system-idle-evt | combine and transform events: the first of several, one with a result changed, one made on demand, one that never or always is ready, a timer |
make-custodian, custodian-shutdown-all, custodian-limit-memory, current-custodian | a custodian owns threads, ports, listeners and other resources; shutting it down releases them all, and its memory limit can be set |
make-plumber, plumber-add-flush! | flush callbacks run when the program exits |
parameterize-break, break-enabled, with-handlers for exn:break? | control when a break can arrive |
#lang racket
(define results (make-channel))
(for ([i (in-range 3)])
(thread (lambda ()
(sleep (* 0.1 (- 3 i)))
(channel-put results i))))
(for/list ([_ (in-range 3)]) (channel-get results)) ; '(2 1 0): the order they finish
(define worker
(thread (lambda ()
(let loop ()
(define msg (thread-receive))
(unless (eq? msg 'stop)
(printf "got ~a\n" msg)
(loop))))))
(thread-send worker 'hello)
(thread-send worker 'stop)
(thread-wait worker)
(sync/timeout 1 (alarm-evt (+ (current-inexact-milliseconds) 100)))
Futures
(future thunk) (from racket/future) starts running the thunk in parallel with the program if the platform allows it; (touch f) waits for its result. A future runs in parallel until it does something that cannot be done safely in parallel (a blocking operation, such as raising an exception or using a continuation), where it stops until touch is called from the thread that made it. In Racket CS, parallelism by futures is normally available on all platforms; in Racket BC it is on by default for Windows, Linux and macOS on x86. (futures-enabled?), (processor-count), (current-future) and would-be-future report and simulate this. make-fsemaphore and its operations are semaphores that futures can use without blocking. for/async and for*/async run the bodies of a loop as futures. Futures share memory with the program, so concurrent assignment to a variable is visible but not synchronised.
Places
A place (from racket/place) is a parallel task that is effectively a separate instance of the Racket virtual machine, in the same operating-system process, with its own memory. Places talk through place channels, which carry only immutable, transparent values, other place channels, and a few shared-memory values (make-shared-bytes, make-shared-flvector, make-shared-fxvector). (place-message-allowed? v) tests whether a value can be sent.
| Function | Does |
|---|---|
(place id body …+) | starts a place that runs the body, with id bound to its place channel; the body must refer to nothing but its imports. place* adds options for the standard ports; place/context keeps the enclosing context |
(dynamic-place module-path 'start-name), dynamic-place* | starts a place from a named function of a module |
place-channel-put, place-channel-get, place-channel-put/get, place-channel | send and receive; make a pair of connected channels |
place-wait, place-kill, place-break, place-dead-evt, place?, place-enabled? | wait for a place to end and get its exit code; end it; test |
#lang racket
(define p
(place ch
(define n (place-channel-get ch))
(place-channel-put ch (* n n))))
(place-channel-put p 12)
(place-channel-get p) ; 144
(place-wait p)
Places are in parallel on every platform where Racket CS runs. The Reference notes that operating-system and garbage-collector limits (a shared page table) can limit how many places scale, perhaps to around 8 or 16.
Which to use
| Need | Use |
|---|---|
| Waiting on I/O, timers, a user interface; many tasks, each mostly idle | threads and sync |
| Number crunching on shared data, with operations that are safe in parallel (flonum and fixnum arithmetic, vectors) | futures, or parallel threads |
| Parallel work with separate state and message passing, or work that uses the full language | places |
Not described
Known, not yet covered
- Reference: memory order and atomic operations, the list of operations that block a future,
racket/place/distributed, thread groups,engine,racket/async-channel's limits,racket/sandbox, and the Racket Guide chapters on parallelism (effective-futures,parallel-threads) were not read. The size and the cost of the parallel thread pools are not described. - Reference: the API of subprocesses and sockets, which also use events, is not described here.
See also
- Control and loops – parameters and
dynamic-wind - I/O and ports – ports as events
- History and versions – Racket CS and BC