【发布时间】:2016-03-18 19:25:21
【问题描述】:
异步通道的“put”操作不会阻塞——除非给定的通道是用缓冲区限制创建的并且已经达到限制。
这是否意味着channel-put 将被阻塞,而其他线程使用channel-get 和async-channel-put 仍在工作而其他线程async-channel-get?
我想知道是否有任何例子可以显示它们的区别?
【问题讨论】:
异步通道的“put”操作不会阻塞——除非给定的通道是用缓冲区限制创建的并且已经达到限制。
这是否意味着channel-put 将被阻塞,而其他线程使用channel-get 和async-channel-put 仍在工作而其他线程async-channel-get?
我想知道是否有任何例子可以显示它们的区别?
【问题讨论】:
您的直觉是正确的:通道往往会阻塞,但异步通道通常不会。为什么?确切的语义是什么?好吧,首先,让我们谈谈普通的渠道。
channel 实际上是 Racket 中的一个原语,与线程一起实现作为跨线程通信的一种方式。它们也是同步的,引用the Racket reference:
频道是同步的;发送方和接收方都必须阻塞,直到(原子)事务完成。多个发送者和接收者可以同时访问一个通道,但每次交易选择一个发送者和接收者。
这意味着通道是相当原始的功能——从通道读取和写入是一个单一的操作,两个线程需要协调,以便它们可以同时发送和接收。
打个比方,通道代表了两个人 Alice 和 Bob 之间的某些项目的转移。双方就会议地点达成一致。如果爱丽丝先到,她会等待鲍勃到达,然后将物品交给鲍勃。如果 Bob 先到,他会等待 Alice 给他物品。转移完成后,两人同时离开。
人是线程,物品是一些球拍价值,会场是渠道。到达会面是从通道读取或写入,必须等待是线程阻塞。离开是线程恢复。
考虑两个线程之间的简单交换:
#lang racket
(define channel (make-channel))
(define alice
(thread (lambda ()
(channel-put channel 'something)
(displayln "Done!"))))
(define bob
(thread (lambda ()
(sleep 5)
(let ([item (channel-get channel)])
(sleep 5)
(displayln item)))))
在上面的例子中,Done! 只会在 5 秒后打印,即使 Alice 线程会立即将'something 放入通道中,而无需等待。由于两个通道需要协调,channel-put 等待另一个线程(在本例中为 Bob)调用channel-get 以便事务发生。
此时,您可能会问自己:为什么 Alice 需要等待?如果爱丽丝能去会面地点,把物品扔进垃圾箱,然后立即离开,那就更好了。然后鲍勃可以在他到达时从垃圾箱中挑选物品,而爱丽丝可以继续她的业务。如果垃圾箱足够大,Alice 甚至可以在 Bob 拿出任何东西之前将多个物品放入其中!
这就是缓冲异步通道的想法。
异步通道是 Racket 中一个简单的衍生概念。它们可以使用内部可变缓冲区作为“bin”在通道之上实现。
异步通道内部由三部分组成:
通道显然只是 Racket 通道,但是我们应该使用什么来缓冲呢?嗯,Racket 实际上提供了an imperative queue implementation in the data/queue module,可以使用,但是 Racket 实现只是在mutable pairs 之上构建了自己的队列。
要管理这三个组件之间的交互,我们只需要一个管理器线程来协调读写操作。事实证明,这个实现相当简单,但我不会在这里重现它。如果您愿意,请查看async-channel.rkt,这是在 Racket 中实现异步通道的模块。它有许多我没有提到的额外好处,但整个实现仍然不到 300 行。
让我们重温一下原来的例子,但是让我们使用异步通道而不是普通通道:
#lang racket
(require racket/async-channel)
(define channel (make-async-channel))
(define alice
(thread (lambda ()
(async-channel-put channel 'something)
(displayln "Done!"))))
(define bob
(thread (lambda ()
(sleep 5)
(let ([item (async-channel-get channel)])
(sleep 5)
(displayln item)))))
现在,Done! 立即打印,因为 puts 不需要阻塞的线程。它只是将其粘贴在内部队列中,不需要关心何时获取值。
默认情况下,将值放入异步通道永远不会阻塞(您可以设置缓冲区大小限制,但这是可选的)。但是,如果内部缓冲区为空,则从异步通道 读取 绝对会阻塞。根据我的经验,这通常是您想要的行为,但如果需要,您可以随时使用 async-channel-try-get 检查值是否已准备就绪。
当然,异步通道也是可变状态,所有关于突变的一般警告也适用于此。值得注意的是,单个通道不能有多个接收器,因为一旦执行了读取操作,该值就会从队列中删除。如果您想要 pub/sub 样式的事件分派,请考虑使用 Multicast Asynchronous Channels 包。
不过,除了陷阱之外,根据我的经验,异步通道几乎总是您想要的。通道是一个重要的原语,但使用起来很棘手。异步通道几乎可以正常工作,它们使多个线程之间的协作变得非常简单。请注意了解它们是如何工作的,这样您就不会在自己的脚下开枪。
【讨论】: