Rust中用于通道的select case范例与Go相当吗?

4

有没有办法像Go一样使用Rust的通道?我找不到任何相关资料。

对于那些不熟悉Go中select语句的人(来自文档):

"select"语句选择一组可能的发送或接收操作中的一个进行。它看起来类似于"switch"语句,但所有情况都涉及通信操作。具有RecvStmt的情况可以将RecvExpr的结果分配给一个或两个变量,可以使用短变量声明声明这些变量。RecvExpr必须是一个(可能带括号的)接收操作。最多只能有一个默认情况,并且可以出现在案例列表的任何位置。
执行"select"语句需要几个步骤:
1. 对于语句中的所有情况,在进入"select"语句时,接收操作的通道操作数以及发送语句的通道和右侧表达式将按源顺序评估一次。结果是一组要从中接收或发送的通道以及相应的值。该评估中的任何副作用都将发生,无论选择哪个(如果有)通信操作进行。具有短变量声明或赋值的RecvStmt左侧的表达式尚未评估。
2. 如果其中一个或多个通信可以进行,则通过统一的伪随机选择选择可以进行的单个通信。否则,如果存在默认情况,则选择该情况。如果没有默认情况,则"select"语句将阻塞,直到至少有一个通信可以进行。
3. 除非所选情况是默认情况,否则将执行相应的通信操作。
4. 如果所选情况是具有短变量声明或赋值的RecvStmt,则将评估左侧表达式并分配接收到的值(或值)。
5. 执行所选情况的语句列表。
由于在nil通道上的通信永远无法进行,因此仅具有nil通道且没有默认情况的select会永久阻塞。
例如,如何在Rust中编写此代码?
func search(ctx context.Context, result chan IResult, q string) error {
    // googleSearch and bingSearch will return IResult interface channel
    google := googleSearch(q)
    bing := bingSearch(q)
    t := time.After(time.Second)

    for {
        select {
           // at any point if caller cancel the operation we return
           case <- ctx.Done():
               return nil
           case r, ok := <- google:
               if !ok { // check if channel is closed
                  google = nil
                  if bing == nil { // we are done
                     return nil
                  }
                  continue
               }
               // sending the google search result to result channel. ( channel to channel )
               result <- r
           case r, ok := <- bing:
               if !ok {
                  bing = nil
                  if google == nil {
                     return nil
                  }
                  continue
               }
               result <- r
           case <- t:
               // this function never lives for more then 1 second
               return fmt.Errorf("timeout")
        }
    }
    return nil
}
2个回答

5
对于标准库中的通道,最有用的答案是“没有”。技术上正确的答案是select!宏:
select! {
    r = result.recv() => {
        // do something
    }
    _ = something_that_waits_a_second.recv() => {
        // timeout
    }
}

(请注意,这是 OP原始示例 在大幅更改之前的等效内容)。
它不稳定,这就是为什么我将其归类为无用的原因。
除了 select! 的稳定性外,还有其他问题。例如,在您的 Go 示例中,您创建了一个神奇地在一段时间后提交到通道的东西(time.After(time.Second))。Rust 没有始终运行的运行时来驱动这些事情。这意味着您需要生成一个操作系统级线程等待一段时间并将值推送到通道中以执行该超时!这非常低效。

如果您真的想要更接近 Go 的绿色线程,我建议您考虑使用 futures:

use futures::{prelude::*, stream}; // 0.3.1
use std::time::Duration;
use tokio::time; // 0.2.10

fn example() -> impl Stream<Item = Result<String, &'static str>> {
    let searches = stream::select(google_search(), bing_search());
    let deadline = time::delay_for(Duration::from_secs(1)).into_stream();

    stream::select(searches.map(Ok), deadline.map(|_| Err("timeout")))
}

#[tokio::main]
async fn main() {
    let result = example()
        .try_for_each(|r| async move {
            println!("Search result: {}", r);
            Ok(())
        })
        .await;

    if let Err(e) = result {
        println!("Had an error: {}", e);
    }
}

fn google_search() -> impl Stream<Item = String> {
    let results = (0..10).map(|x| format!("goog{}", x));
    let results = stream::iter(results);

    // Fake some delay between results
    let delay = time::interval(Duration::from_millis(75));
    results.zip(delay).map(|(r, _)| r)
}

fn bing_search() -> impl Stream<Item = String> {
    let results = (0..10).map(|x| format!("bing{}", x));
    let results = stream::iter(results);

    // Fake some delay between results
    let delay = time::interval(Duration::from_millis(200));
    results.zip(delay).map(|(r, _)| r)
}
#[tokio::main] 注解在每个 Go 程序中是隐式的;它会启动一个异步反应器并将其“运行到完成”。

“没有一个”那么,recv_timeout怎么样?如果可以在库中实现,我们就不需要特殊的语法或宏。 - mcarton
@mcarton 我认为一个展示这个的答案会很有用!我对Go不了解得足够多,无法确定Go的select能否在Rust中轻松实现。 - Shepmaster
2
crossbeam_channel 似乎也有类似于 Go 的 channel 和 select。但是我对 Go 的了解基本上止步于 "lol no generics" - mcarton
@mcarton:那么你会很高兴知道,针对Go 2.0,Go团队正在研究错误处理和泛型的语法糖 :) - Matthieu M.

5
如果你只想设置超时接收,那么recv_timeout方法就是你要寻找的。
如果你想在不阻塞的情况下接收数据,则有try_recv方法。
如果你想混合多个通道,那么@Shepmaster's answer描述了我所知道的标准库中唯一的方法。
然而,Rust的标准库比Go的轻量得多,在Rust中,使用crate来完成这种操作是很常见的。 crossbeam_channel crate具有一个select方法,它是稳定的,并可用于等待不同的通道。

1
我认为try_recv需要被放在一个循环中,依次尝试每个通道直到其中有数据。我相信这会导致处理器使用率达到100%。 - Shepmaster
3
crossbeam 的 select 实现非常复杂。 - Shepmaster
2
@Shepmaster,try_recv更多的是替换此示例中第一次使用select。对于像select这样的宏,我也不指望更多:D - mcarton

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接