Racket编程指南——18 并发与同步

18 并发与同步

Racket以线程(threads)的形式提供了并发(concurrency), 并且它提供了一个通用的sync(同步)函数,可以用于同步线程和其它隐式并发表,如端口(ports)

线程并发运行的意义是,一个线程可以在不进行协作的情况下抢占另一个线程,但线程不会在使用多个硬件处理器的情况下并行运行。有关Racket中并行的信息,请参见《并行》。

18.1 线程

要同时执行一个过程,请使用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")

18.2 线程邮箱

每个线程都有一个用于接收消息的邮箱。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)))

18.3 信号

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

信号是一种底层技术。通常,更好的解决方案是将资源访问限制为单个线程。例如,通过有一个用于打印输出的专用线程,可以更好地实现对标准输出的同步访问。

18.4 通道

当一个值从一个线程传递到另一个线程时,通道(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)

18.5 缓冲异步通道

缓冲异步通道与上述通道类似,但是异步通道的“放置(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))

请注意,上面的例子缺少任何同步来验证是否处理了所有项目。如果主线程在没有同步的情况下退出,则工作线程可能无法完成某些项目的处理,或者打印线程无法打印所有项目。

18.6 可同步事件和sync

还有其它方法可以同步线程。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)异常。

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值