为什么`futures :: channel :: mpsc`只能通知一个发送者?

时间:2018-11-11 04:40:28

标签: concurrency rust rust-tokio

我正在阅读futures-preview 0.3资料,以查找如何正确“通知任何人”。在mpsc::channel(有界)中,多个发送者可以等待接收(如果缓冲区已满)。

考虑到next_messageunpark_one的实现,接收者似乎每一张收据仅通知一个发送者。

我怀疑这会以select!的形式出现,因为select!可能会导致虚假通知。但是,我无法提出问题案例。

这是我试图混淆mpsc

[package]
name = "futures-mpsc-test"
version = "0.1.0"
edition = "2018"

[dependencies]
futures-preview = { version = "0.3.0-alpha.9", features = ["tokio-compat"] }
tokio = "0.1.11"

这:

#![feature(async_await, await_macro, futures_api, pin)]

use std::collections::HashSet;

use futures::prelude::*;

use futures::channel::mpsc::{channel, Sender};
use futures::channel::oneshot;
use futures::select;

async fn main2() {
    let channel_len = 1;
    let num_false_wait = 1000;
    let num_expected_messages = 100;

    let (mut send, mut recv) = channel(channel_len);
    // One extra capacity per sender. Fill the extras.
    await!(send.send(-2)).unwrap();

    // Fill buffers
    for _ in 0..channel_len {
        await!(send.send(-1)).unwrap();
    }

    // False waits. Should resolve and produce false waiters.
    for _ in 0..num_false_wait {
        await!(false_wait(&send));
    }

    // True messages.
    {
        let mut send = send.clone();
        await!(send.send(-2)).unwrap();
        tokio::spawn(async move {
            for i in 0..num_expected_messages {
                await!(send.send(i)).unwrap();
            }
            Ok(())
        }.boxed().compat());
    }

    // Drain receiver until all true messages are received.
    let mut expects = (0..num_expected_messages).collect::<HashSet<_>>();
    while !expects.is_empty() {
        let i = await!(recv.next()).unwrap();
        expects.remove(&i);
        eprintln!("Received: {}", i);
    }
}

// If `send` is full, it will produce false waits.
async fn false_wait(send: &Sender<i32>) {
    let (wait_send, wait_recv) = oneshot::channel();
    let mut send = send.clone();
    await!(send.send(-2)).unwrap();
    tokio::spawn(async move {
        let mut sending = send.send(-3);
        let mut fallback = future::ready(());
        select! {
            sending => {
                sending.unwrap();
            },
            fallback => {
                eprintln!("future::ready is selected");
            },
        };
        wait_send.send(()).unwrap();
        Ok(())
    }.boxed().compat());
    await!(wait_recv).unwrap();
}

fn main() {
    tokio::run(async {
        await!(main2());
        Ok(())
    }.boxed().compat());
}

我希望这会发生:

  1. 缓冲区由-1填充。因此,以后的发件人被阻止。
  2. 同时有“真服务员”和“假服务员”。 错误的服务员已经退出,因为select!的另一分支 立即完成。
  3. 在每次致电await!(recv.next())时,最多只有一个 等待发送者 通知。如果通知了错误的服务员,则没有人可以将其推送到缓冲区, 即使缓冲区有一个空房间。
  4. 如果耗尽所有元素而没有真实通知, 整个系统都卡住了。

尽管我期望如此,main2异步功能已成功完成。为什么?

2 个答案:

答案 0 :(得分:1)

  

我怀疑这会在select!的意义上起作用,因为select!可能导致错误的通知。

否,您无法使用mpsc“混淆”一个select!频道:

select!不会触发任何与mspc相关的通知,它只会返回最先完成的未来。

消息队列已满时,await!(recv.next())会通知一个生产者现在有一个进入有界通道的插槽。

换句话说:没有true waitersfalse waiters: 当通道消息队列已满时,生产方将阻塞并等待接收方使用已排队的消息。

答案 1 :(得分:1)

进一步研究futures源代码解决了我的问题。最后,我不能以此方式混淆mpsc。

重点在于,mpsc的大小是灵活的,并且可以比最初指定的大小增长更多。此行为是mentioned in the docs

  

该频道的容量等于buffer + num-senders。换句话说,每个发送方在信道容量上都有一个保证的时隙,最重要的是,所有发送方都可以使用缓冲区“先到先服务”的时隙。

是的,我在进行实验之前已经先阅读了这篇文章,但是当时我还不知道它的重要性。

固定缓冲区问题

考虑一个典型的有界队列实现,其中队列的大小不能超过初始指定的大小。规格是这样的:

  • 当队列为空时,接收方阻塞。
  • 当队列已满(即大小已达到界限)时,发件人将阻塞。

在这种情况下,如果队列已满,则多个发件人正在等待单个资源(队列的大小)。

在多线程编程中,这是通过诸如notify_one之类的原语来完成的。但是,在futures中,这是容易犯错的:与多线程编程不同,通知任务不一定使用资源,因为任务可能已经放弃了获取资源(由于到select!Deadline之类的结构),然后规范就被破坏了(队列未满,但所有活动的发件人都被阻止了)。

mpsc很灵活

如上所述, futures::channel::mpsc::channel的缓冲区大小并不严格。规范总结为:

  • message_queue.len() == 0时,接收方会阻止。
  • message_queue.len() >= buffer时,发件人可能被阻止。
  • message_queue.len() >= buffer + num_senders时,发件人会阻止。

在这里,num_senders基本上是Sender的克隆数量,但在某些情况下要多一些。更准确地说,num_sendersSenderTask的数量。

那么,如何避免资源共享?我们还有其他状态:

  • 每个发件人(SenderTask的实例)的布尔状态为is_parked
  • 该频道还有一个称为parked_queue的队列,是ArcSenderTask的引用的队列。

通道维护以下不变式:

  • message_queue.len() <= buffer + num_parked_senders。请注意,我们不知道num_parked_senders的值。
  • parked_queue.len() == min(0, message_queue.len() - buffer)
  • 每个驻留的发件人在parked_queue中至少有一条消息。

这是通过以下算法完成的:

  • 为了接收,
    • 它从SenderTask中弹出parked_queue,如果寄件人已停放,请取消停放。
  • 要发送,
    • 它总是等待is_parkedfalse。如果message_queue.len() < bufferparked_queue.len() == 0一样,则所有发件人都将被停放。因此,在这种情况下,我们可以保证取得进展。
    • 如果is_parkedfalse,则无论如何将消息推送到队列。
    • 此后,如果为message_queue.len() <= buffer,则无需进一步操作。
    • 如果message_queue.len() > buffer,则将发件人置于不停车状态并推到parked_queue

您可以轻松地检查上述算法中保持的不变性。

令人惊讶的是,发件人不再等待共享资源。而是,发送者等待其is_parked状态。即使发送任务在完成前就被丢弃,它也将在parked_queue中保留一段时间,不会阻塞任何内容。多么聪明!