Rust, Future, Recursion, Send

这几天写了个爬虫, 遇到了一个Future中的变量不满足Send trait导致无法编译通过的问题,挺有意思,记录一下。

以下是简要复盘当时的现场:

fn f1<'a>() -> BoxFuture<'a, ()> {
    async move {
        let p = Rc::new(1);
        f2().await;
    }
    .boxed()
}

async fn f2() {
    println!("hell world");
}

以下是编译器的提示:

future cannot be sent between threads safely
within `impl futures::Future`, the trait `std::marker::Send` is not implemented for `Rc<i32>`

 

简单说一下出现这种问题的原因, 上一篇文章里面提到过,Future其实就是个状态机, 扔到线程上就可以跑,但是中途是可以yield的, 然后等到某些条件达到之后再唤醒线程, 从yield出去的那个点继续执行下去, 不过虽说叫`唤醒`线程, 但实际上不是Future直接与操作系统沟通来完成线程调度的,这个工作是由runtime来完成的(tokio之类的),  这些runtime一般自己会维护一个线程池, 具体怎么跟物理线程对应咱就不清楚了,但是在这里可以暂时把这个池子里的线程理解为物理线程, runtime从池子里拉一个闲着的线程来继续执行Future, 这就导致一个问题, 这个线程很有可能不是上一个执行这个Future的线程, 也就是说,中途换人了。本来这事儿跟用户没有任何关系,你runtime就是一中间层, 干的就是调用system call实现调度的活,至于怎么干,我不关心。我买个冰箱还得懂制冷,没这个道理。但是现实是, 你还就真得照顾一下这些runtime的脾气,把你的代码改成符合它们审美的样子。

回到上面这段代码中来, 因为Rc是!Send的, 所以在走到下面的await的时候,变量p是无法从一个线程安全的转移给另一个线程的。所以编译器给出了这样的提示。一开始看到这提示的时候我想着既然不是Send那就定义个自己的struct把用到的那些变量都改成Send的不就好了么, 结果改了几个小时放弃了, 因为用到的第三方库的struct太复杂了。后来想着搜索一下看看别人是怎么解决这类问题的,结果看到了一个貌似属于官方的答案:

async fn foo() {
    {
        let x = NotSend::default();
    }
    bar().await;
}

刚看到这个答案的时候也是挺无语的, 你们这是在解决问题么?这不应该叫掩盖问题么?编译器是不报错了,可我也用不了这些变量了,我下面的调用还指望这些变量呢。后来又一琢磨, 不对,人家的意思不是这个, 是让你把那些!Send的变量的处理放到一个Scope里,如果后续需要那就以Send的形式暴露出来就可以了:

fn f1<'a>() -> BoxFuture<'a, ()> {
    async move {
        let pp = {
            let p = Rc::new(1);
            *p
        };
        f2().await;
    }
    .boxed()
}

async fn f2() {
    println!("hell world");
}

当然, 你定义一个fn来把中间那部分代码块换掉也可以,看上去还会简洁一些。总之,await的时候,只要其他活着的变量都是Send的就好, 至于那些!Send的变量,那不好意思,要么让它去死要么让它换个Send的壳再出来。

这个问题貌似还有另一种解决办法, 就是不用boxed(),而是用boxed_local()

fn f1() -> Pin<Box<Future<Output = ()>>> {
    async move {
        let p = Rc::new(1);
        f2().await;
    }
    .boxed_local()
}

async fn f2() {
    println!("hell world");
}

这样编译也能通过, 但是这个Pin应该会把整个这个Future固定到某个内存位置上,至于对性能有什么影响,后面有时间再研究。

这个问题只在我写递归异步调用的时候出现,因为如果不是递归调用的话我也不用费劲返回BoxFuture了, 直接用async fn就好了。开始我以为这是所有的Future所需要遵守的规则,但是今天我试了一下,如果直接用async fn f1() {}这种形式的话,编译器是不会报错的,这个也留到后面研究吧。

这个爬虫前前后后写了三天,大部分时间都在解决各种代码层面的问题,最直接的感受就是,Rust的Future真的不如Go的Goroutine好用, Go的runtime真的是替你解决了绝大部分的问题,大部分的时候你都不必知道它的存在,而且它也很智能,会在各种阻塞的系统调用的时候自动进行Goroutine的调度,这些在你的代码里都不需要体现。看着一个普通的函数或者方法,只要增加上合适的同步机制,就可以完美的转化成一个可并发的函数或方法。在这点上,Rust实现起来则要复杂的多,这里说的复杂倒不是机制上的复杂,而是各种细节上的复杂。这让用户写起来真的很烦。

 

下面附上这个爬虫的代码,里面还有不少小问题,但是已经可以成功跑完了, 有兴趣的朋友可以看一下给提点意见:https://github.com/wangjun861205/motospec_scraper_rs

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:游动-白 设计师:我叫白小胖 返回首页

打赏作者

wangjun861205

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值