18 Concurrency and Synchronization
Racket 以 thread 的形式提供 concurrency,并提供一个通用的 sync 函数, 可用于同步 thread 以及其他隐式的并发形式,例如 port。
Thread 以并发方式运行,即一个 thread 可以在另一个 thread 无协作的情况下抢占它, 但默认情况下,thread 不会以使用多个硬件处理器的方式并行运行。这种默认类型的 thread 称为 coroutine thread。有关 Racket 中并行性的信息,请参见 Parallelism。
18.1 Threads
要并发执行一个过程,请使用 thread。以下示例从主线程创建两个新 thread:
(displayln "This is the original thread") (thread (lambda () (displayln "This is a new thread."))) (thread (lambda () (displayln "This is another new thread.")))
下一个示例创建一个新 thread,该 thread 本应无限循环,但主线程使用 sleep 暂停自身 2.5 秒, 然后使用 kill-thread 终止工作 thread:
(define worker (thread (lambda () (let loop () (displayln "Working...") (sleep 0.2) (loop))))) (sleep 2.5) (kill-thread worker)
在 DrRacket 中,主线程会一直运行直到点击 Stop 按钮,因此在 DrRacket 中不需要 thread-wait。
如果主线程完成或被终止,应用程序将退出,即使其他 thread 仍在运行。 一个 thread 可以使用 thread-wait 等待另一个 thread 完成。 这里,主线程使用 thread-wait 确保工作 thread 在主线程退出之前完成:
(define worker (thread (lambda () (for ([i 100]) (printf "Working hard... ~a~n" i))))) (thread-wait worker) (displayln "Worker finished")
要从一个 thread 接收结果,请在创建该 thread 时使用 #:keep 'results, 然后 thread-wait 可以返回该 thread 的过程所返回的值:
(define worker (thread (lambda () (+ 1 2)) #:keep 'results)) (thread-wait worker)
18.2 Thread Mailboxes
每个 thread 都有一个用于接收消息的邮箱。thread-send 函数异步地向另一个 thread 的邮箱发送消息, 而 thread-receive 则从当前 thread 的邮箱中返回最早的消息,必要时会阻塞等待消息。 在以下示例中,主线程向工作 thread 发送数据进行处理,然后在没有更多数据时发送 'done 消息, 并等待工作 thread 完成。
(define worker-thread (thread (lambda () (let loop () (match (thread-receive) [(? number? num) (printf "Processing ~a~n" num) (loop)] ['done (printf "Done~n")]))))) (for ([i 20]) (thread-send worker-thread i)) (thread-send worker-thread 'done) (thread-wait worker-thread)
在下一个示例中,主线程将工作委托给多个算术 thread,然后等待接收结果。 算术 thread 处理工作项,然后将结果发送回主线程。
(define (make-arithmetic-thread operation) (thread (lambda () (let loop () (match (thread-receive) [(list oper1 oper2 result-thread) (thread-send result-thread (format "~a ~a ~a = ~a" oper1 (object-name operation) oper2 (operation oper1 oper2))) (loop)]))))) (define addition-thread (make-arithmetic-thread +)) (define subtraction-thread (make-arithmetic-thread -)) (define worklist '((+ 1 1) (+ 2 2) (- 3 2) (- 4 1))) (for ([item worklist]) (match item [(list '+ o1 o2) (thread-send addition-thread (list o1 o2 (current-thread)))] [(list '- o1 o2) (thread-send subtraction-thread (list o1 o2 (current-thread)))])) (for ([i (length worklist)]) (displayln (thread-receive)))
18.3 Semaphores
Semaphore 有助于对任意共享资源进行同步访问。 当多个 thread 必须对单个资源执行非原子操作时,请使用 semaphore。
在以下示例中,多个 thread 并发地向标准输出打印。如果没有同步,一个 thread 打印的行 可能会出现在另一个 thread 打印的行的中间。通过使用一个以 1 初始化的 semaphore, 同一时间只有一个 thread 会打印。semaphore-wait 函数会阻塞直到 semaphore 的内部计数器 非零,然后递减计数器并返回。semaphore-post 函数递增计数器, 以便另一个 thread 可以解除阻塞并打印。
(define output-semaphore (make-semaphore 1)) (define (make-thread name) (thread (lambda () (for [(i 10)] (semaphore-wait output-semaphore) (printf "thread ~a: ~a~n" name i) (semaphore-post output-semaphore))))) (define threads (map make-thread '(A B C))) (for-each thread-wait threads)
等待 semaphore、执行工作、然后向 semaphore 发送信号的模式也可以使用 call-with-semaphore 来表达,其优势在于当控制逃离时(例如由于异常) 会自动向 semaphore 发送信号:
(define output-semaphore (make-semaphore 1)) (define (make-thread name) (thread (lambda () (for [(i 10)] (call-with-semaphore output-semaphore (lambda () (printf "thread ~a: ~a~n" name i))))))) (define threads (map make-thread '(A B C))) (for-each thread-wait threads)
Semaphore 是一种低级技术。通常,更好的解决方案是将资源访问限制在单个 thread 中。 例如,对标准输出的同步访问可以通过使用一个专门用于打印输出的 thread 来更好地实现。
18.4 Channels
Channel 在两个 thread 之间同步,同时将一个值从一个 thread 传递到另一个 thread。 与 thread 邮箱不同,多个 thread 可以从单个 channel 获取项, 因此当多个 thread 需要从单个工作队列中消费项时,应使用 channel。
在以下示例中,主线程使用 channel-put 将项添加到 channel, 而多个工作 thread 使用 channel-get 消费这些项。 对任一过程的每次调用都会阻塞,直到另一个 thread 使用相同的 channel 调用另一个过程。 工作 thread 处理这些项,然后通过 result-channel 将结果传递给结果 thread。
(define result-channel (make-channel)) (define result-thread (thread (lambda () (let loop () (display (channel-get result-channel)) (loop))))) (define work-channel (make-channel)) (define (make-worker thread-id) (thread (lambda () (let loop () (define item (channel-get work-channel)) (case item [(DONE) (channel-put result-channel (format "Thread ~a done\n" thread-id))] [else (channel-put result-channel (format "Thread ~a processed ~a\n" thread-id item)) (loop)]))))) (define work-threads (map make-worker '(1 2))) (for ([item '(A B C D E F G H DONE DONE)]) (channel-put work-channel item)) (for-each thread-wait work-threads) (channel-put result-channel "") ; waits until result-thread has printed all other output
18.5 Buffered Asynchronous Channels
缓冲异步 channel 与上面描述的 channel 类似,但异步 channel 的 “put” 操作不会阻塞—— 除非该 channel 创建时设置了缓冲区限制且已达到限制。 异步 put 操作因此与 thread-send 类似,但与 thread 邮箱不同, 异步 channel 允许多个 thread 从单个 channel 中消费项。
在以下示例中,主线程向工作 channel 添加项,该 channel 最多同时容纳三个项。 工作 thread 从这个 channel 中处理项,然后将结果发送给打印 thread。
(require racket/async-channel) (define print-thread (thread (lambda () (let loop () (displayln (thread-receive)) (loop))))) (define (safer-printf . items) (thread-send print-thread (apply format items))) (define work-channel (make-async-channel 3)) (define (make-worker-thread thread-id) (thread (lambda () (let loop () (define item (async-channel-get work-channel)) (safer-printf "Thread ~a processing item: ~a" thread-id item) (loop))))) (for-each make-worker-thread '(1 2 3)) (for ([item '(a b c d e f g h i j k l m)]) (async-channel-put work-channel item))
注意上面的示例缺少任何同步来验证所有项都已被处理。如果主线程在没有这种同步的情况下退出, 工作 thread 可能不会完成处理某些项,或者打印 thread 不会打印所有项。
18.6 Synchronizable Events and sync
还有其他同步 thread 的方式。sync 函数允许 thread 通过 synchronizable event 进行协调。 许多值同时充当 event,允许以统一的方式使用不同类型来同步 thread。 event 的示例包括 channel、port、thread 和 alarm。本节通过多个示例展示 event、thread 和 sync(以及递归函数)的组合如何允许你实现 任意复杂的通信协议来协调程序的并发部分。
在下一个示例中,channel 和 alarm 被用作 synchronizable event。 工作 thread 对两者进行 sync,以便它们可以处理 channel 项直到 alarm 被激活。 Channel 项被处理后,结果被发送回主线程。
(define main-thread (current-thread)) (define alarm (alarm-evt (+ 3000 (current-inexact-milliseconds)))) (define channel (make-channel)) (define (make-worker-thread thread-id) (thread (lambda () (define evt (sync channel alarm)) (cond [(equal? evt alarm) (thread-send main-thread 'alarm)] [else (thread-send main-thread (format "Thread ~a received ~a" thread-id evt))])))) (make-worker-thread 1) (make-worker-thread 2) (make-worker-thread 3) (channel-put channel 'A) (channel-put channel 'B) (let loop () (match (thread-receive) ['alarm (displayln "Done")] [result (displayln result) (loop)]))
下一个示例展示了一个用于简单 TCP echo 服务器的函数。该函数使用 sync/timeout 对来自给定 port 的输入或 thread 邮箱中的消息进行同步。sync/timeout 的第一个参数 指定应在给定 event 上等待的最大秒数。read-line-evt 函数返回一个 event, 当给定输入 port 中有可用的行输入时该 event 就绪。thread-receive-evt 的结果在 thread-receive 不会阻塞时就绪。在实际应用中,thread 邮箱中接收的消息 可用于控制消息等。
(define (serve in-port out-port) (let loop [] (define evt (sync/timeout 2 (read-line-evt in-port 'any) (thread-receive-evt))) (cond [(not evt) (displayln "Timed out, exiting") (tcp-abandon-port in-port) (tcp-abandon-port out-port)] [(string? evt) (fprintf out-port "~a~n" evt) (flush-output out-port) (loop)] [else (printf "Received a message in mailbox: ~a~n" (thread-receive)) (loop)])))
serve 函数用于以下示例中,该示例启动一个服务器 thread 和一个客户端 thread, 它们通过 TCP 通信。客户端向服务器打印三行,服务器将其回显。 客户端的 copy-port 调用会阻塞直到收到 EOF。服务器在两秒后超时, 关闭 port,这使得 copy-port 可以完成,客户端可以退出。 主线程使用 thread-wait 等待客户端 thread 退出 (因为如果没有 thread-wait,主线程可能在其他 thread 完成之前退出)。
(define port-num 4321) (define (start-server) (define listener (tcp-listen port-num)) (thread (lambda () (define-values [in-port out-port] (tcp-accept listener)) (serve in-port out-port)))) (start-server) (define client-thread (thread (lambda () (define-values [in-port out-port] (tcp-connect "localhost" port-num)) (display "first\nsecond\nthird\n" out-port) (flush-output out-port) ; copy-port will block until EOF is read from in-port (copy-port in-port (current-output-port))))) (thread-wait client-thread)
有时,你希望将结果行为直接附加到传递给 sync 的 event 上。 在以下示例中,工作 thread 对三个 channel 进行同步,但每个 channel 必须以不同方式处理。 使用 handle-evt 可以为给定 event 关联一个回调。 当 sync 选择给定 event 时,它会调用回调来生成同步结果, 而不是使用 event 的正常同步结果。由于 event 在回调中处理, 因此不需要对 sync 的返回值进行分派。
(define add-channel (make-channel)) (define multiply-channel (make-channel)) (define append-channel (make-channel)) (define (work) (let loop () (sync (handle-evt add-channel (lambda (list-of-numbers) (printf "Sum of ~a is ~a~n" list-of-numbers (apply + list-of-numbers)))) (handle-evt multiply-channel (lambda (list-of-numbers) (printf "Product of ~a is ~a~n" list-of-numbers (apply * list-of-numbers)))) (handle-evt append-channel (lambda (list-of-strings) (printf "Concatenation of ~s is ~s~n" list-of-strings (apply string-append list-of-strings))))) (loop))) (define worker (thread work)) (channel-put add-channel '(1 2)) (channel-put multiply-channel '(3 4)) (channel-put multiply-channel '(5 6)) (channel-put add-channel '(7 8)) (channel-put append-channel '("a" "b"))
handle-evt 的结果在 sync 的尾部位置调用其回调, 因此可以安全地使用如以下示例中的递归。
(define control-channel (make-channel)) (define add-channel (make-channel)) (define subtract-channel (make-channel)) (define (work state) (printf "Current state: ~a~n" state) (sync (handle-evt add-channel (lambda (number) (printf "Adding: ~a~n" number) (work (+ state number)))) (handle-evt subtract-channel (lambda (number) (printf "Subtracting: ~a~n" number) (work (- state number)))) (handle-evt control-channel (lambda (kill-message) (printf "Done~n"))))) (define worker (thread (lambda () (work 0)))) (channel-put add-channel 2) (channel-put subtract-channel 3) (channel-put add-channel 4) (channel-put add-channel 5) (channel-put subtract-channel 1) (channel-put control-channel 'done) (thread-wait worker)
wrap-evt 函数类似于 handle-evt,但其 handler 不在 sync 的尾部位置被调用。 同时,wrap-evt 在其 handler 调用期间禁用 break 异常。
18.7 Building Your Own Synchronization Patterns
Event 还允许你编码程序的多个并发部分之间的不同通信模式。 一种常见的模式是生产者-消费者模式。以下是使用上述思想实现其变体的一种方式。 一般来说,这些通信模式通过一个服务器循环来实现,该循环使用 sync 等待任意数量的不同可能性发生,然后对其做出反应,更新一些本地状态。
(define/contract (produce x) (-> any/c void?) (channel-put producer-chan x))
(define/contract (consume) (-> any/c) (channel-get consumer-chan))
; private state and server loop
(define producer-chan (make-channel)) (define consumer-chan (make-channel)) (void (thread (λ () ; the items variable holds the items that ; have been produced but not yet consumed (let loop ([items '()]) (sync ; wait for production (handle-evt producer-chan (λ (i) ; if that event was chosen, ; we add an item to our list ; and go back around the loop (loop (cons i items)))) ; wait for consumption, but only ; if we have something to produce (handle-evt (if (null? items) never-evt (channel-put-evt consumer-chan (car items))) (λ (_) ; if that event was chosen, ; we know that the first item item ; has been consumed; drop it and ; and go back around the loop (loop (cdr items)))))))))
; an example (non-deterministic) interaction
> (void (thread (λ () (sleep (/ (random 10) 100)) (produce 1))) (thread (λ () (sleep (/ (random 10) 100)) (produce 2)))) > (list (consume) (consume)) '(1 2)
可以构建更复杂的同步模式。这是一个简单的示例, 我们在生产者-消费者的基础上扩展了一个等待至少生产了特定数量项的操作。
(define/contract (produce x) (-> any/c void?) (channel-put producer-chan x)) (define/contract (consume) (-> any/c) (channel-get consumer-chan)) (define/contract (wait-at-least n) (-> natural? void?) (define c (make-channel)) ; we send a new channel over to the ; main loop so that we can wait here (channel-put wait-at-least-chan (cons n c)) (channel-get c))
(define producer-chan (make-channel)) (define consumer-chan (make-channel)) (define wait-at-least-chan (make-channel)) (void (thread (λ () (let loop ([items '()] [total-items-seen 0] [waiters '()]) ; instead of waiting on just production/ ; consumption now we wait to learn about ; threads that want to wait for a certain ; number of elements to be reached (apply sync (handle-evt producer-chan (λ (i) (loop (cons i items) (+ total-items-seen 1) waiters))) (handle-evt (if (null? items) never-evt (channel-put-evt consumer-chan (car items))) (λ (_) (loop (cdr items) total-items-seen waiters))) ; wait for threads that are interested ; the number of items produced (handle-evt wait-at-least-chan (λ (waiter) (loop items total-items-seen (cons waiter waiters)))) ; for each thread that wants to wait, (for/list ([waiter (in-list waiters)]) ; we check to see if there has been enough ; production (cond [(<= (car waiter) total-items-seen) ; if so, we send a message back on the channel ; and continue the loop without that item (handle-evt (channel-put-evt (cdr waiter) (void)) (λ (_) (loop items total-items-seen (remove waiter waiters))))] [else ; otherwise, we just ignore that one never-evt])))))))
; an example (non-deterministic) interaction
> (define thds (for/list ([i (in-range 10)]) (thread (λ () (produce i) (wait-at-least 10) (display (format "~a -> ~a\n" i (consume)))))))
> (for ([thd (in-list thds)]) (thread-wait thd))
9 -> 2
5 -> 3
0 -> 1
8 -> 0
6 -> 9
7 -> 8
2 -> 7
4 -> 6
1 -> 5
3 -> 4