18 并发与同步
Racket以线程(threads)的形式提供了并发(concurrency), 并且它提供了一个通用的sync(同步)函数,可以用于同步线程和其它隐式并发表,如端口(ports)。
线程并发运行的意义是,一个线程可以在不进行协作的情况下抢占另一个线程,但线程不会在使用多个硬件处理器的情况下并行运行。有关Racket中并行的信息,请参见《并行》。
要同时执行一个过程,请使用thread(线程)。以下示例从主线程创建两个新线程:
(displayln "This is the original thread") (thread (lambda () (displayln "This is a new thread."))) (thread (lambda () (displayln "This is another new thread.")))
下一个示例创建了一个原本会无限循环的新线程,但主线程使用sleep使其自身暂停2.5秒,然后使用kill-thread终止了worker线程:
(define worker (thread (lambda () (let loop () (displayln "Working...") (sleep 0.2) (loop))))) (sleep 2.5) (kill-thread worker)
在DrRacket中,主线程一直运行,直到单击Stop(停止)按钮,因此在DrRacket中,不需要使用thread-wait(线程等待)。
如果主线程结束或被终止,即使其它线程仍在运行,应用程序也会退出。线程可以使用thread-wait来等待另一个线程完成。这里,主线程使用thread-wait来确保worker线程在主线程退出之前完成:
(define worker (thread (lambda () (for ([i 100]) (printf "Working hard... ~a~n" i))))) (thread-wait worker) (displayln "Worker finished")
每个线程都有一个用于接收消息的邮箱。thread-send函数异步地将消息发送到另一个线程的邮箱, 而thread-receive从当前线程的邮箱中返回最旧的消息,如果需要的话则阻塞以等待消息。在下面的示例中,主线程将数据发送给要处理的工作线程(worker thread),然后在没有更多数据时发送'done消息,并等待工作线程完成。
(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)
在下一个示例中,主线程将工作委托给多个算术线程,然后等待接收结果。算术线程处理工作项,然后将结果发送到主线程。
(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" oper1 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)))
信号(Semaphores)有助于同步访问任意共享资源。当多个线程必须对单个资源执行非原子操作时,应该使用信号。
在下面的示例中,多个线程同时打印到标准输出。如果不同步,一个线程打印的行可能出现在另一个线程打印行的中间。通过使用一个用1计数初始化的信号,一次只能打印一个线程能。semaphore-wait函数会阻塞,直到信号的内部计数器为非零,然后递减计数器并返回。semaphore-post函数递增计数器,以便另一个线程可以解除阻塞,然后打印。
(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)
等待信号、工作和发布到信号量的模式也可以使用call-with-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)
信号是一种底层技术。通常,更好的解决方案是将资源访问限制为单个线程。例如,通过有一个用于打印输出的专用线程,可以更好地实现对标准输出的同步访问。
当一个值从一个线程传递到另一个线程时,通道(Channels)同步两个线程。与线程邮箱不同,多个线程可以从单个通道获取项目,因此当多个线程需要从单个工作队列中接受项目时,应该使用通道。
在下面的示例中,主线程使用channel-put将项目添加到一个通道中,而多个工作线程使用channel-get来消耗这些项目。对任一过程的每个调用都会阻塞,直到另一个线程使用相同的通道调用另一个过程。worker处理这些项目,然后通过result-channel将结果传递给结果线程。
(define result-channel (make-channel)) (define result-thread (thread (lambda () (let loop () (displayln (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" thread-id))] [else (channel-put result-channel (format "Thread ~a processed ~a" 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)
缓冲异步通道与上述通道类似,但是异步通道的“放置(put)”操作不会阻塞,除非给定通道是用缓冲区限制创建的,并且已达到限制。因此,异步放置(asynchronous-put)操作有点儿类似于thread-send, 但与线程邮箱不同,异步通道允许多个线程从单个通道中消耗项目。
在下面的示例中,主线程将项目添加到工作通道(work channel),该通道一次最多容纳三个项目。工作线程(worker 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))
请注意,上面的例子缺少任何同步来验证是否处理了所有项目。如果主线程在没有同步的情况下退出,则工作线程可能无法完成某些项目的处理,或者打印线程无法打印所有项目。
还有其它方法可以同步线程。sync函数允许线程通过同步事件(synchronizable events)进行协调。许多值兼作事件,允许以统一的方式使用不同类型同步过程。事件示例包括通道、端口、线程和警报。
在下一个例子中,通道和警报用作可同步事件。工作人员(workers)在两个频道上都sync,这样他们就可以处理通道项目,直到警报启动。处理通道项目,然后将结果发送回主线程。
(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回显服务器的函数。该函数使用sync/timeout来同步来自给定端口的输入或线程邮箱中的消息。sync/timeout的第一个参数指定了在给定事件上应该等待的最大秒数。当给定输入端口中有一行输入可用时,read-line-evt函数返回一个准备就绪的事件。当调用thread-receive不会阻塞时,thread-receive-evt的结果为准备就绪。在实际应用程序中,线程邮箱中接到的消息可用于控制消息,等等。
(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函数,它启动通过TCP通信的服务器线程和客户端线程。客户端将三行打印到服务器端,服务器端将其回传。客户端的copy-port调用会阻塞,直到收到EOF。服务器端在两秒钟后超时,关闭端口,这允许copy-port完成并退出客户端。主线程使用thread-wait等待客户端线程退出(因为如有thread-wait,主线程可能会在其它线程完成之前退出)。
(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将阻塞,直到从端口中读取EOF (copy-port in-port (current-output-port))))) (thread-wait client-thread)
有时,你希望将结果行为直接附加到传递给sync的事件上。在下面的示例中,工作线程在三个通道上同步,但每个通道必须以不同的方式处理。使用handle-evt将回调与给定事件相关联。当sync选择给定的事件时,它调用回调来生成同步结果,而不是使用事件的正常同步结果。由于事件是在回调中处理,因此不需要对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,只是它的处理程序不是在相对于sync的尾部被调用。同时,wrap-evt在其处理程序调用期间禁用中断(break)异常。