< 返回版块

teshin 发表于 2023-06-28 15:30

Tags:rust,channel,异步

@TOC

前言

使用通信来共享内存,而不是通过共享内存来通信

上面这句话,是每个go开发者在 处理多线程通信时 的座右铭,go甚至把实现这个理念的channel直接焊在编译器里,几乎所有的go程序里都有channel的身影。 rust的异步和go的goroutine有异曲同工之妙,甚至可以把 tokio::spawn 理解为go关键字。但在rust中好像并没有异步channel的实现。本着求人不如求己的原则,决定diy一个类似go的channel。

思路

先看一下发送流程

再看一下接收流程

总体来说流程清晰易懂,不管接收还是发送,都是先尝试从缓存队列中操作值,不成功则加入到对应队列,等待再次执行。反之则唤起相关任务,结束操作。

实现功能

  1. 首先需要实现一个存放值的环形缓冲区,并且每个单元应该是单独加锁的,从而避免全局锁。
  2. 需要两个任务队列,用来存放在饥饿模式(从缓存操作失败)下的 发送任务和接受任务。
  3. 按照rust习惯,将发送者和接受者拆开,并各自实现future
  4. 因为唤醒不是同步的,需要通过一个唤醒器来唤醒沉默的任务。
  5. 使用原子操作替代锁

代码实现

具体的就不写了,放在github上了 github地址:https://github.com/woshihaoren4/wd_tools/tree/main/src/channel

测试

这里主要和async-channel测试一下

  • async-channel 是最常见的异步channel,在crateio上有两千万的下载。

先引测试版包

cargo.toml

[dependencies]
tokio = {version = "1.22.0",features=["full"]}
wd_tools = {version = "0.8.3",features = ["sync","chan"]}
async-channel = "1.8.0"
  • wd_tools 是我们的channel,这里引用的sync chan两个feature,前者用于测试,后者是chan实现。

测试代码

测试场景:设置缓存长度为10,发100万数据,接100万数据。在1发送者1接受者,1发送者10接受者,10发送者1接受者,10发送者10接受者四种情况下的收发性能。

use std::fmt::Debug;
use wd_tools::channel as wd;
use async_channel as ac;

#[tokio::main]
async fn main(){
    let ts = TestService::new(10);
    println!("test start ------------> wd_tools");
    ts.send_to_recv("1-v-1",true,100_0000,1,100_0000,1,|x|x).await;
    ts.send_to_recv("1-v-10",true,100_0000,1,10_0000,10,|x|x).await;
    ts.send_to_recv("10-v-1",true,10_0000,10,100_0000,1,|x|x).await;
    ts.send_to_recv("10-v-10",true,10_0000,10,10_0000,10,|x|x).await;
    println!("wd_tools <------------- test over");
    println!("test start ------------> async-channel");
    ts.send_to_recv("1-v-1",false,100_0000,1,100_0000,1,|x|x).await;
    ts.send_to_recv("1-v-10",false,100_0000,1,10_0000,10,|x|x).await;
    ts.send_to_recv("10-v-1",false,10_0000,10,100_0000,1,|x|x).await;
    ts.send_to_recv("10-v-10",false,10_0000,10,10_0000,10,|x|x).await;
    println!("async-channel <------------ test over");
}

struct TestService<T>{
    wd_sender : wd::Sender<T>,
    wd_receiver : wd::Receiver<T>,
    ac_sender : ac::Sender<T>,
    ac_receiver : ac::Receiver<T>
}

impl<T:Unpin+Send+Sync+Debug+'static> TestService<T>{
    pub fn new(cap:usize)->TestService<T>{
        let (wd_sender,wd_receiver) = wd::Channel::new(cap);
        let (ac_sender,ac_receiver) = ac::bounded(cap);
        TestService{wd_sender,wd_receiver,ac_sender,ac_receiver}
    }

    pub fn send<G:Fn(usize)->T+Send+Sync+'static>(&self,wg:wd_tools::sync::WaitGroup,is_wd:bool,max:usize,generater:G){
        let wd_sender = self.wd_sender.clone();
        let ac_sender = self.ac_sender.clone();

        wg.defer_args1(|is_wd|async move{
            for i in 0..max {
                let t = generater(i);
                if is_wd {
                    wd_sender.send(t).await.expect(" 发送失败");
                }else{
                    ac_sender.send(t).await.expect(" 发送失败");
                }
            }
        },is_wd);
    }
    pub fn recv(&self,wg:wd_tools::sync::WaitGroup,is_wd:bool,max:usize){
        let wd_receiver = self.wd_receiver.clone();
        let ac_receiver = self.ac_receiver.clone();

        wg.defer_args1(|is_wd|async move{
            for _i in 0..max {
                if is_wd {
                    wd_receiver.recv().await.expect(" 接收失败");
                }else{
                    ac_receiver.recv().await.expect(" 接收失败");
                }
            }
        },is_wd);
    }

    pub async fn send_to_recv<G:Fn(usize)->T+Send+Sync+Clone+'static>(&self,info:&'static str, is_wd:bool, sbase:usize, sgroup:usize, rbase:usize, rgroup:usize, generater:G){
        let now = std::time::Instant::now();
        let wg = wd_tools::sync::WaitGroup::default();
        let wg_send = wd_tools::sync::WaitGroup::default();
        let wg_recv = wd_tools::sync::WaitGroup::default();

        for _ in 0..sgroup{
            self.send(wg_send.clone(),is_wd,sbase,generater.clone());
        }
        for _ in 0..rgroup{
            self.recv(wg_recv.clone(),is_wd,rbase);
        }

        wg.defer(move ||async move{
            let now = std::time::Instant::now();
            wg_send.wait().await;
            println!("test[{}] ---> send use time:{}ms",info,now.elapsed().as_millis());
        });
        wg.defer(move ||async move{
            let now = std::time::Instant::now();
            wg_recv.wait().await;
            println!("test[{}] ---> recv use time:{}ms",info,now.elapsed().as_millis());
        });

        wg.wait().await;
        println!("test[{}] ---> all use time:{}ms",info,now.elapsed().as_millis());
    }
}

结果与分析

测试10次,取平均值做表,如下 在这里插入图片描述 如上图,得结论

  • 在1发收者和10发收者的情况下,两种channel效率相差不多。
  • 在发送者和接受者数量不等时,wd_tools::channel的性能明显优于async-channel

思考

分析结论之前先看一下async-channel的实现。虽然async-channel也是异步,但它并不依赖某个异步运行时来进行任务的上线文切换,而是使用concurrent-queueevent-listener进行消息调度,底层依赖于std::thread::park_timeout

相比event-listener的调度方式,直接管理tokio的Context则更适用于异步环境。尤其是存在大量等待的场景。如上面测试,接受者和发送者数量不等,需要长时间等待的情况。实际开发中,接受者或者发送者可能长时间处于饥饿的情况下,wd_tools::channel不会产生多余的资源开销,毕竟上下文被挂起了,也就不会被cpu执行。

当然实际是复杂的,因情而异,使用的CPU数量(线程数),缓存长度,异步任务数同样会影响消息队列的性能,尤其是不需要等待的场景下async-channel性能更优。

wd_tools::channel则更适合tokio异步环境。并且不会引起线程park,而产生其他影响。

尾语

wd_tools::channel 目前只是一个初级版本,还有很多地方待优化,比如过多的状态判断,对缓存区直接轮训加锁,而没有采用优化算法, 唤醒器完全可以通过一定优化策略替换带。 但这个思路是没错的,欢迎有想法的同志加入进来。

博客链接地址已变更


Ext Link: https://juejin.cn/post/7249794087075643450

评论区

写评论
作者 teshin 2023-06-29 17:33

tokio并没有实现类似go的channel,就是一个多生产者多消费者通道,其中只有一个消费者看到每条消息。 在这个链接的文章里是这样说的 If you need a multi-producer multi-consumer channel where only one consumer sees each message, you can use the async-channel crate. There are also channels for use outside of asynchronous Rust, such as std::sync::mpsc and crossbeam::channel. These channels wait for messages by blocking the thread, which is not allowed in asynchronous code.

LongRiver 2023-06-29 16:44

请问下,我理解tokio自带了一个异步的channel,https://tokio.rs/tokio/tutorial/channels。但你说在Rust没有找到异步的channel。是我哪里理解有误吗?

作者 teshin 2023-06-29 10:28

准备换了

--
👇
廴壬吉: 支持,用博客园吧

作者 teshin 2023-06-29 10:27

你说的对,我也正准备换一个博客地址

--
👇
ssrlive: 你可以在任何網站上寫博客文章,但請不要在 csdn 上. 這個網站是如此的垃圾, 如此的流氓: 不注冊賬號不讓複製網頁文字, 不讓複製代碼, 不讓下載文件. 而注冊賬號呢, 强行索要 電話號碼, 而且還明文存儲密碼, 毫無隱私可言. 如此垃圾的網站, 還希望你儘早遠離.

墻内有非常多的競品, 墻外也有非常多的競品. 用什麽不是用呢?

廴壬吉 2023-06-28 22:10

支持,用博客园吧

ssrlive 2023-06-28 17:23

你可以在任何網站上寫博客文章,但請不要在 csdn 上. 這個網站是如此的垃圾, 如此的流氓: 不注冊賬號不讓複製網頁文字, 不讓複製代碼, 不讓下載文件. 而注冊賬號呢, 强行索要 電話號碼, 而且還明文存儲密碼, 毫無隱私可言. 如此垃圾的網站, 還希望你儘早遠離.

墻内有非常多的競品, 墻外也有非常多的競品. 用什麽不是用呢?

1 共 6 条评论, 1 页