20 Parallelism

▼ |  | The Racket Guide

1 | Welcome to Racket
2 | Racket Essentials
3 | Built-In Datatypes
4 | Expressions and Definitions
5 | Programmer-Defined Datatypes
6 | Modules
7 | Contracts
8 | Input and Output
9 | Regular Expressions
10 | Exceptions and Control
11 | Iterations and Comprehensions
12 | Pattern Matching
13 | Classes and Objects
14 | Units
15 | Reflection and Dynamic Evaluation
16 | Macros
17 | Creating Languages
18 | Concurrency and Synchronization
19 | Performance
20 | Parallelism
21 | Running and Creating Executables
22 | More Libraries
23 | Dialects of Racket and Scheme
24 | Command-Line Tools and Your Editor of Choice
| Bibliography
| Index

► | 20 | Parallelism

20.1 | Parallel Threads
20.2 | Parallelism with Futures
20.3 | Parallelism with Places
20.4 | Distributed Places

On this page:

20.1 Parallel Threads
20.2 Parallelism with Futures
20.3 Parallelism with Places
20.4 Distributed Places

top contents ← prev up next →

## 20 Parallelism🔗ℹ

Racket provides three forms of parallelism: parallel threads, futures, and places. On a platform that provides multiple processors, parallelism can improve the run-time performance of a program.

See also Performance for information on sequential performance in Racket. Racket also provides threads for concurrency, but coroutine threads for concurrency do not provide parallelism; see Concurrency and Synchronization for more information.

### 20.1 Parallel Threads🔗ℹ

A thread that is created with a parallel thread pool can use a different hardware processor than other threads, providing parallelism in addition to concurrency. When hardware is available, using these parallel threads can reduce the latency of a computation.

> > > A trade-off with parallel threads is that a computation may instead get slower, instead of faster, due to overhead creating and managing parallel tasks.

> > > The BC implementation of Racket treats all threads as coroutine threads and does not run them in parallel.

Create a parallel thread using the #:pool argument to thread, where 'own means that the thread gets its own, single-thread pool.

> (define (fib n)
> (cond
> [(= n 0) 1]
> [(= n 1) 1]
> [else (+ (fib (- n 1)) (fib (- n 2)))]))
> (define n1 (thread (lambda () (fib 35))
> #:pool 'own
> #:keep 'results))
> (define n2 (thread (lambda () (fib 35))
> #:pool 'own
> #:keep 'results))
> (time (= (thread-wait n1)
> (thread-wait n2)))

Whether parallel threads improve performance depends on the nature of the computation. The kinds of computations that can run efficiently in parallel are mostly the same ones that can run in a future, and Parallelism with Futures provides more information and describes how the futures visualizer can help explain a program’s performance. The difference with a parallel thread is that when a future would block, a parallel thread instead synchronizes enough with other threads to continue—in the hope of quickly reaching a point that is clear of blocking actions. Also, a parallel thread can freely use parameters and exception handlers, which are limited in futures until a touch provides a full dynamic context to the future. Finally, parallel threads implement the same interface for synchronization as coroutine threads, including thread-wait, thread-send, and kill-thread.

Racket’s predefined constructs, such as input and output ports or mutable hash tables, are thread-safe and use locks internally as needed. Threads can perform some input and output operations in parallel, especially on regular files that never block. Meanwhile, blocking input and output operations not only interfere with parallelism, they can be significantly more expensive for parallel threads than for coroutine threads, because extra synchronization is required. Operations that interact with thread scheduling, such sleep, sync, thread, thread-wait, kill-thread, and custodian-shutdown-all, all require synchronization that can be more expensive when triggered from a parallel thread.

The semaphore-post and semaphore-wait operations can be inexpensive in a parallel thread as long as no thread ends up blocking on the semaphore. When a thread is blocked on a semaphore, then operations on the semaphore interact with thread scheduling and are therefore slower in parallel threads. In some cases, using a future semaphore (which is more limited than a normal semaphore) can provide better performance for parallel threads.

### 20.2 Parallelism with Futures🔗ℹ

The racket/future library provides support for performance improvement through parallelism with futures and the future and touch functions. Achieving parallelism through those constructs depends on avoiding blocking operations: anything that inspects the full continuation or requires atomic execution relative to Racket threads. The caveats in Performance in DrRacket also apply to futures; notably, debugging instrumentation currently defeats futures.

> > > Other functions, such as thread, support the creation of reliably concurrent tasks.

As a starting example, the any-double? function below takes a list of numbers and determines whether any number in the list has a double that is also in the list:

> (define (any-double? l)
> (for/or ([i (in-list l)])
> (for/or ([i2 (in-list l)])
> (= i2 (* 2 i)))))

This function runs in quadratic time, so it can take a long time (on the order of a second) on large lists like l1 and l2:

> (define l1 (for/list ([i (in-range 5000)])
> (+ (* 2 i) 1)))
> (define l2 (for/list ([i (in-range 5000)])
> (- (* 2 i) 1)))
> (or (any-double? l1)
> (any-double? l2))

The best way to speed up any-double? is to use a different algorithm. However, on a machine that offers at least two processing units, the example above can run in about half the time using future and touch:

> (let ([f (future (lambda () (any-double? l2)))])
> (or (any-double? l1)
> (touch f)))

The future f runs (any-double? l2) in parallel to (any-double? l1), and the result for (any-double? l2) becomes available about the same time that it is demanded by (touch f).

Futures run in parallel as long as they can do so safely and independent of any continuation context that a touch might provide. Continuation context can include an exception handler, parameter value, continuation prompts, or other values accessible via continuation marks. Safety concerns shared mutable objects, such as input ports, output ports, and equal?-based hash tables, where concurrent access is managed either internally or explicitly via locks. The remainder of this section works through an example to illustrate safety and continuation obstacles, and it also shows how the futures visualizer (provided by future-visualizer) can help shed light on those obstacles.

Consider the following core of a Mandelbrot-set computation:

> (define (mandelbrot iterations x y n)
> (printf "starting\n")
> (let ([ci (- (/ (* 2.0 y) n) 1.0)]
> [cr (- (/ (* 2.0 x) n) 1.5)])
> (let loop ([i 0] [zr 0.0] [zi 0.0])
> (if (> i iterations)
> i
> (let ([zrq (* zr zr)]
> [ziq (* zi zi)])
> (cond
> [(> (+ zrq ziq) 4) i]
> [else (loop (add1 i)
> (+ (- zrq ziq) cr)
> (+ (* 2 zr zi) ci))]))))))

The expressions (mandelbrot 10000000 62 500 1000) and (mandelbrot 10000000 62 501 1000) each take a while to produce an answer. Computing them both, of course, takes twice as long:

> (list (mandelbrot 10000000 62 500 1000)
> (mandelbrot 10000000 62 501 1000))

Unfortunately, attempting to run the two computations in parallel by using one future does not improve performance:

> (let ([f (future (lambda () (mandelbrot 10000000 62 501 1000)))])
> (list (mandelbrot 10000000 62 500 1000)
> (touch f)))

To see why, use the futures visualizer from future-visualizer to visualize the execution of the above program. The visualizer opens a window showing a graphical view of a trace of the computation. The upper-left portion of the window contains an execution timeline:

Each horizontal row represents a parallel task, and the colored dots represent important events in the execution of the program; they are color-coded to distinguish one event type from another. The upper-left blue dot in the timeline represents the future’s creation. The future executes for a brief period (represented by a green bar in the second line) on thread 1. It then pauses, because the Racket thread will need to perform a future-unsafe operation (as represented by a red dot). That pause is long, because the Racket thread is performing its own copy of the calculation before it touches the future. Meanwhile, the pink vertical lines represent garbage-collection events, which imply a synchronization across parallel tasks.

A future-unsafe operation or an operation that needs continuation context is a blocking operation. A blocking operation halts evaluation of a future and will not allow it to continue until the future is touched. A touch of the future in a thread causes its work to be evaluated sequentially by the thread, which provides a continuation context and can synchronize with Racket threads.In the BC implementation of Racket, a synchronized operation also halts the future. A background thread may perform the operation at any time and, once completed, the future may continue running in parallel. The CS implementation can perform synchronized operations without stopping a future or relying on a background thread.

When you move your mouse over an event, the visualizer shows you detailed information about the event and draws arrows connecting all of the events in the corresponding future. This image shows those connections for our future.

A dotted blue line connects the first event in the future to the future that created it, a red line connects a future blocking event to its resumptions, and the purple lines connect adjacent events within the future.

The reason that we see no parallelism is that the printf operation before the loop in mandelbrot needs to look up the current-output-port parameter’s value, which depends on the evaluation context of the touch. Even if that is fixed by using fprintf and variable that refers directly to the port, writing to a port is also a blocking operation, because it must take a lock on the port. Removing the printf avoids both problems.

More generally, we can create N futures to perform the same computation, and they will run in parallel:

> (define fs
> (for/list ([i (in-range N)])
> (future (lambda () (mandelbrot 10000000 62 500 1000)))))
> (for/list ([f (in-list fs)])
> (touch f))

With N as 4 on a machine with at least 4 processing units:

Most every arithmetic operation in this example produces an inexact number whose storage must be allocated, and that triggers frequent garbage collections as reflected by dense pink lines, effectively giving the whole graph a pink background. Garbage collection is not necessarily a problem, but since a garbage collection requires synchronization across parallel tasks, it can sometimes limit performance.

By using flonum-specific operations (see Fixnum and Flonum Optimizations), we can re-write mandelbrot to use much less allocation:

> (define (mandelbrot iterations x y n)
> (let ([ci (fl- (fl/ (* 2.0 (->fl y)) (->fl n)) 1.0)]
> [cr (fl- (fl/ (* 2.0 (->fl x)) (->fl n)) 1.5)])
> (let loop ([i 0] [zr 0.0] [zi 0.0])
> (if (> i iterations)
> i
> (let ([zrq (fl* zr zr)]
> [ziq (fl* zi zi)])
> (cond
> [(fl> (fl+ zrq ziq) 4.0) i]
> [else (loop (add1 i)
> (fl+ (fl- zrq ziq) cr)
> (fl+ (fl* 2.0 (fl* zr zi)) ci))]))))))

This conversion can speed mandelbrot by a factor of 10 or so, even in sequential mode, but avoiding allocation also allows mandelbrot to run more consistently in parallel. Executing this program yields the following pink-free result the visualizer (not to scale relative to previous graphs):

As a general guideline, an operation is blocking if it needs to consult the continuation (such as obtaining a parameter value) or if it interacts with Racket’s thread system, such as taking a lock within the implementation of an output port or an equal?-based hash table. In the CS implementation of Racket, most primitives are non-blocking, while the BC implementation includes many more blocking or synchronized operations.

### 20.3 Parallelism with Places🔗ℹ

The racket/place library provides support for performance improvement through parallelism with the place form. The place form creates a place, which is effectively a new Racket instance that can run in parallel to other places, including the initial place. The full power of the Racket language is available at each place, but places can communicate only through message passing—using the place-channel-put and place-channel-get functions on a limited set of values—which helps ensure the safety and independence of parallel computations.

As a starting example, the racket program below uses a place to determine whether any number in the list has a double that is also in the list:

> #lang racket
> (provide main)
> (define (any-double? l)
> (for/or ([i (in-list l)])
> (for/or ([i2 (in-list l)])
> (= i2 (* 2 i)))))
> (define (main)
> (define p
> (place ch
> (define l (place-channel-get ch))
> (define l-double? (any-double? l))
> (place-channel-put ch l-double?)))
> (place-channel-put p (list 1 2 4 8))
> (place-channel-get p))

The identifier ch after place is bound to a place channel. The remaining body expressions within the place form are evaluated in a new place, and the body expressions use ch to communicate with the place that spawned the new place.

In the body of the place form above, the new place receives a list of numbers over ch and binds the list to l. It then calls any-double? on the list and binds the result to l-double?. The final body expression sends the l-double? result back to the original place over ch.

In DrRacket, after saving and running the above program, evaluate (main) in the interactions window to create the new place. When using places inside DrRacket, the module containing place code must be saved to a file before it will execute. Alternatively, save the program as "double.rkt" and run from a command line with

racket -tm double.rkt

where the -t flag tells racket to load the double.rkt module, the -m flag calls the exported main function, and -tm combines the two flags.

The place form has two subtle features. First, it lifts the place body to an anonymous, module-level function. This lifting means that any binding referenced by the place body must be available in the module’s top level. Second, the place form dynamic-requires the enclosing module in a newly created place. As part of the dynamic-require, the current module body is evaluated in the new place. The consequence of this second feature is that place should not appear immediately in a module or in a function that is called in a module’s top level; otherwise, invoking the module will invoke the same module in a new place, and so on, triggering a cascade of place creations that will soon exhaust memory.

> #lang racket
> (provide main)
> ; Don't do this!
> (define p (place ch (place-channel-get ch)))
> (define (indirect-place-invocation)
> (define p2 (place ch (place-channel-get ch))))
> ; Don't do this, either!
> (indirect-place-invocation)

### 20.4 Distributed Places🔗ℹ

The racket/place/distributed library provides support for distributed programming.

The example below demonstrates how to launch a remote racket node instance, launch remote places on the new remote node instance, and start an event loop that monitors the remote node instance.

The example code can also be found in "racket/distributed/examples/named/master.rkt".

> > > #lang racket/base
> > > (require racket/place/distributed
> > > racket/class
> > > racket/place
> > > racket/runtime-path
> > > "bank.rkt"
> > > "tuple.rkt")
> > > (define-runtime-path bank-path "bank.rkt")
> > > (define-runtime-path tuple-path "tuple.rkt")
> > > (provide main)
> > > (define (main)
> > > (define remote-node (spawn-remote-racket-node
> > > "localhost"
> > > #:listen-port 6344))
> > > (define tuple-place (supervise-place-at
> > > remote-node
> > > #:named 'tuple-server
> > > tuple-path
> > > 'make-tuple-server))
> > > (define bank-place (supervise-place-at
> > > remote-node bank-path
> > > 'make-bank))
> > > (message-router
> > > remote-node
> > > (after-seconds 4
> > > (displayln (bank-new-account bank-place 'user0))
> > > (displayln (bank-add bank-place 'user0 10))
> > > (displayln (bank-removeM bank-place 'user0 5)))
> > > (after-seconds 2
> > > (define c (connect-to-named-place remote-node
> > > 'tuple-server))
> > > (define d (connect-to-named-place remote-node
> > > 'tuple-server))
> > > (tuple-server-hello c)
> > > (tuple-server-hello d)
> > > (displayln (tuple-server-set c "user0" 100))
> > > (displayln (tuple-server-set d "user2" 200))
> > > (displayln (tuple-server-get c "user0"))
> > > (displayln (tuple-server-get d "user2"))
> > > (displayln (tuple-server-get d "user0"))
> > > (displayln (tuple-server-get c "user2"))
> > > )
> > > (after-seconds 8
> > > (node-send-exit remote-node))
> > > (after-seconds 10
> > > (exit 0))))

> Figure 1: examples/named/master.rkt

The spawn-remote-racket-node primitive connects to "localhost" and starts a racloud node there that listens on port 6344 for further instructions. The handle to the new racloud node is assigned to the remote-node variable. Localhost is used so that the example can be run using only a single machine. However localhost can be replaced by any host with ssh publickey access and racket. The supervise-place-at creates a new place on the remote-node. The new place will be identified in the future by its name symbol 'tuple-server. A place descriptor is expected to be returned by invoking dynamic-place with the tuple-path module path and the 'make-tuple-server symbol.

The code for the tuple-server place exists in the file "tuple.rkt". The "tuple.rkt" file contains the use of define-named-remote-server form, which defines a RPC server suitable for invocation by supervise-place-at.

> > > #lang racket/base
> > > (require racket/match
> > > racket/place/define-remote-server)
> > > (define-named-remote-server tuple-server
> > > (define-state h (make-hash))
> > > (define-rpc (set k v)
> > > (hash-set! h k v)
> > > v)
> > > (define-rpc (get k)
> > > (hash-ref h k #f))
> > > (define-cast (hello)
> > > (printf "Hello from define-cast\n")
> > > (flush-output)))

> Figure 2: examples/named/tuple.rkt

The define-named-remote-server form takes an identifier and a list of custom expressions as its arguments. From the identifier a place-thunk function is created by prepending the make- prefix. In this case make-tuple-server. The make-tuple-server identifier is the place-function-name given to the supervise-named-dynamic-place-at form above. The define-state custom form translates into a simple define form, which is closed over by the define-rpc form.

The define-rpc form is expanded into two parts. The first part is the client stubs that call the rpc functions. The client function name is formed by concatenating the define-named-remote-server identifier, tuple-server, with the RPC function name set to form tuple-server-set. The RPC client functions take a destination argument which is a remote-connection% descriptor and then the RPC function arguments. The RPC client function sends the RPC function name, set, and the RPC arguments to the destination by calling an internal function named-place-channel-put. The RPC client then calls named-place-channel-get to wait for the RPC response.

The second expansion part of define-rpc is the server implementation of the RPC call. The server is implemented by a match expression inside the make-tuple-server function. The match clause for tuple-server-set matches on messages beginning with the 'set symbol. The server executes the RPC call with the communicated arguments and sends the result back to the RPC client.

The define-cast form is similar to the define-rpc form except there is no reply message from the server to client

> > > (module tuple racket/base
> > > (require racket/place
> > > racket/match)
> > > (define/provide
> > > (tuple-server-set dest k v)
> > > (named-place-channel-put dest (list 'set k v))
> > > (named-place-channel-get dest))
> > > (define/provide
> > > (tuple-server-get dest k)
> > > (named-place-channel-put dest (list 'get k))
> > > (named-place-channel-get dest))
> > > (define/provide
> > > (tuple-server-hello dest)
> > > (named-place-channel-put dest (list 'hello)))
> > > (define/provide
> > > (make-tuple-server ch)
> > > (let ()
> > > (define h (make-hash))
> > > (let loop ()
> > > (define msg (place-channel-get ch))
> > > (define (log-to-parent-real
> > > msg
> > > #:severity (severity 'info))
> > > (place-channel-put
> > > ch
> > > (log-message severity msg)))
> > > (syntax-parameterize
> > > ((log-to-parent (make-rename-transformer
> > > #'log-to-parent-real)))
> > > (match
> > > msg
> > > ((list (list 'set k v) src)
> > > (define result (let () (hash-set! h k v) v))
> > > (place-channel-put src result)
> > > (loop))
> > > ((list (list 'get k) src)
> > > (define result (let () (hash-ref h k #f)))
> > > (place-channel-put src result)
> > > (loop))
> > > ((list (list 'hello) src)
> > > (define result
> > > (let ()
> > > (printf "Hello from define-cast\n")
> > > (flush-output)))
> > > (loop))))
> > > loop))))

> Figure 3: Expansion of define-named-remote-server

top contents ← prev up next →