< 返回版块

zhuxiujia 发表于 2020-10-21 22:27

Tags:关于future异步运行时的猜想

这几年,似乎服务器开发领域一直在向异步发展。

一开始java系netty开始流行nio 从nodejs 开始,开始流行promise,回调地狱迫使它支持了async await golang看不下去了,用CSP模型 实现了彻底的异步化 最后出场的Rust贯彻零开销概念,也实现了基于future的async await

后来 一些开源库作者蠢蠢欲动,

async_std 欲成为rust的异步标准库,tokio凭借mio以及大名鼎鼎的actix-web谁与争锋。 后起之秀may 欲实现go的CSP模型却苦于固定容量栈

最后,作为开发者的你 大概使用rust也懵逼了吧??? 比如我,我希望异步库性能高,也希望0开销,似乎2者不可兼得。

例如,基于future的tokio 和基于csp模型的may性能上肯定有一点点差距, 在我看来,IO的时间越长,future的优点体现出来了,它可以hold住更多的IO同时保持低消耗。 但是 IO的时间如果越短,future的弱点就出来了,反而基于线程模型或者CSP模型的吞吐量更快更高

忽然在想,是否能有基于时间评估的异步运行时呢,例如500ms时间内在线程池里执行,超过500ms移动到协程池里执行

评论区

写评论
Ryan-Git 2020-10-26 21:23

要测切换开销也是多个线程/协程切,而不是单个在那边 yield。。。这能一样么。

”但是IO的时间如果越短,future的弱点就出来了“ 有啥理论依据?假设一个 io 瞬间完成,无论操作系统还是 async runtime 都有能力在原地继续执行,没啥本质区别。

作者 zhuxiujia 2020-10-24 15:22

测试数量提高到10万(数量多一些),换windows10测试,并且使用--release

use std::thread::sleep;
use std::time::{Duration, SystemTime};
use std::ops::Sub;

//计算每个操作耗时nano纳秒
fn use_time(total: i32, start: SystemTime, end: SystemTime) {
    let t= end.duration_since(start).unwrap();
    println!("use Time: {:?} s,each:{} nano/op", &t, t.as_nanos() / (total as u128));
}

//use Time: 20.9887ms s,each:209 nano/op
//cargo run --release --color=always --package bench --bin bench
fn main() {
    println!("Hello, world!");
    let total = 100000;
    let now = SystemTime::now();
    for _ in 0..total {
        //IO time
        std::thread::yield_now();
    }
    let end = SystemTime::now();
    use_time(total, now, end);
}

//use Time: 70.4634ms s,each:704 nano/op
//cargo test --release --color=always --package bench --bin bench tokio_bench --no-fail-fast -- --exact -Z unstable-options --show-output
#[tokio::test]
async fn tokio_bench() {
    let total = 100000;
    let now = SystemTime::now();
    for _ in 0..total {
        //IO time
        tokio::task::yield_now().await;
    }
    let end = SystemTime::now() ;
    use_time(total, now, end);
    //use Time: 2.001 s,each:2001000 nano/op
}

//use Time: 11.7917ms s,each:117 nano/op
//cargo test --release --color=always --package bench --bin bench async_bench --no-fail-fast -- --exact -Z unstable-options --show-output
#[async_std::test]
async fn async_bench() {
    let total = 100000;
    let now = SystemTime::now();
    for _ in 0..total {
        //IO time
        async_std::task::yield_now().await;
    }
    let end = SystemTime::now();
    use_time(total, now, end);
}

--
👇
zhuxiujia: 改了下实现算法

[dependencies]
tokio = { version = "0.2", features = ["full"] }
async-std = { version = "1.6", features = ["attributes"] }
use std::thread::sleep;
use std::time::{Duration, SystemTime};
use std::ops::Sub;

//计算每个操作耗时nano纳秒
fn use_time(total: i32, start: SystemTime, end: SystemTime) {
    let t= end.duration_since(start).unwrap();
    println!("use Time: {:?} s,each:{} nano/op", &t, t.as_nanos() / (total as u128));
}

fn main() {
    println!("Hello, world!");
    let total = 1000;
    let now = SystemTime::now();
    for _ in 0..total {
        //IO time
        std::thread::yield_now();
    }
    let end = SystemTime::now();
    use_time(total, now, end);
    //use Time: 1.189 s,each:1189000 nano/op
}

#[tokio::test]
async fn tokio_bench() {
    let total = 1000;
    let now = SystemTime::now();
    for _ in 0..total {
        //IO time
        tokio::task::yield_now().await;
    }
    let end = SystemTime::now() ;
    use_time(total, now, end);
    //use Time: 2.001 s,each:2001000 nano/op
}

#[async_std::test]
async fn async_bench() {
    let total = 1000;
    let now = SystemTime::now();
    for _ in 0..total {
        //IO time
        async_std::task::yield_now().await;
    }
    let end = SystemTime::now();
    use_time(total, now, end);
}

--
👇
zhuxiujia: 4核,我这里测smol和async_std 差不了多少

--
👇
vimcdoe: 补上smol
https://github.com/vimcdoe/yield_now_test
cpu 多少核的 我这里测的smol最快

作者 zhuxiujia 2020-10-23 21:36

4核,我这里测smol和async_std 差不了多少

--
👇
vimcdoe: 补上smol
https://github.com/vimcdoe/yield_now_test
cpu 多少核的 我这里测的smol最快

vimcdoe 2020-10-23 21:14

补上smol
https://github.com/vimcdoe/yield_now_test
cpu 多少核的 我这里测的smol最快

作者 zhuxiujia 2020-10-23 16:12

补上async_std的yield

//async_std:    async_std::task::yield_now().await;
use Time: 0.003 s,each:3000 nano/op

--
👇
zhuxiujia: 你确定???


  //thread :  yield_now();
 //use Time: 0.002 s,each:2000 nano/op

 //tokio:   tokio::task::yield_now().await;
 //use Time: 0.036 s,each:36000 nano/op

--
👇
gwy15: 把 sleep_for 改成 tokio::task::yield_now()std::thread::yield_now(),现在是纯粹切换上下文了,结果:

async
use Time: 29598400 ns, each: 29.5984 nano/op
thread
use Time: 573740300 ns, each: 573.7403 nano/op

https://gist.github.com/gwy15/0cfdae1dedf059252202a9b15c246386

作者 zhuxiujia 2020-10-23 16:08

你确定???


  //thread :  yield_now();
 //use Time: 0.002 s,each:2000 nano/op

 //tokio:   tokio::task::yield_now().await;
 //use Time: 0.036 s,each:36000 nano/op

--
👇
gwy15: 把 sleep_for 改成 tokio::task::yield_now()std::thread::yield_now(),现在是纯粹切换上下文了,结果:

async
use Time: 29598400 ns, each: 29.5984 nano/op
thread
use Time: 573740300 ns, each: 573.7403 nano/op

https://gist.github.com/gwy15/0cfdae1dedf059252202a9b15c246386

gwy15 2020-10-23 15:51

把 sleep_for 改成 tokio::task::yield_now()std::thread::yield_now(),现在是纯粹切换上下文了,结果:

async
use Time: 29598400 ns, each: 29.5984 nano/op
thread
use Time: 573740300 ns, each: 573.7403 nano/op

https://gist.github.com/gwy15/0cfdae1dedf059252202a9b15c246386

作者 zhuxiujia 2020-10-23 15:37

测试不算特别严谨,但是反应出tokio 的02版本调度性能不给力呀。比go和async_std差一倍。 正规的IO测试应该发起一个模拟http请求之类的。总的来说,短IO肯定是线程更快一点点

--
👇
gwy15: 你这就一个线程,反复等 delay,然后把总时间除以等待的总时间,误差也太大了。

tokio 的 tokio::time::delay_for 很清楚地写了,

The delay operates at millisecond granularity and should not be used for tasks that require high-resolution timers.

如果要比较“上下文切换”的速度,应该比较的是高数量的线程/协程切换时间,而不是单个线程反复等低精度的计时器。

gwy15 2020-10-23 15:31

你这就一个线程,反复等 delay,然后把总时间除以等待的总时间,误差也太大了。

tokio 的 tokio::time::delay_for 很清楚地写了,

The delay operates at millisecond granularity and should not be used for tasks that require high-resolution timers.

如果要比较“上下文切换”的速度,应该比较的是高数量的线程/协程切换时间,而不是单个线程反复等低精度的计时器。

作者 zhuxiujia 2020-10-23 15:11

再试一下golang的模拟

func Benchmark_IO(b *testing.B) {
	b.StopTimer()
	b.StartTimer()
	for i := 0; i < b.N; i++ {
                //IO time
		time.Sleep(time.Millisecond)
	}
}
//Benchmark_SqlBuilder-4   	     970	   1178489 ns/op

--
👇
zhuxiujia:

use std::thread::sleep;
use std::time::Duration;
use chrono::Local;


fn use_tps(total: i32, start: i64, end: i64) {
    let mut time = (end - start) as f64;
    time = time / 1000.0;
    println!("use TPS: {} TPS/s", total as f64 / time);
}

//计算每个操作耗时nano纳秒
fn use_time(total: i32, start: i64, end: i64) {
    let mut time = (end - start) as f64;
    time = time / 1000.0;
    println!("use Time: {} s,each:{} nano/op", time, time * 1000000000.0 / (total as f64));
}


fn main() {
    println!("Hello, world!");
    let total = 1000;
    let now = Local::now().timestamp_millis();
    for _ in 0..total {
        //IO time
        sleep(Duration::from_millis(1));
    }
    let end = Local::now().timestamp_millis();
    use_time(total, now, end);
    //use Time: 1.189 s,each:1189000 nano/op
}

#[tokio::test]
async fn tokio_bench() {
    let total = 1000;
    let now = Local::now().timestamp_millis();
    for _ in 0..total {
        //IO time
        tokio::time::delay_for(Duration::from_millis(1)).await;
    }
    let end = Local::now().timestamp_millis();
    use_time(total, now, end);
    //use Time: 2.001 s,each:2001000 nano/op
}

--
👇
gwy15: 你做个减法啊, 协程是 4309-2 = 4307,线程是 17352-312=17040……

--
👇
zhuxiujia: 代码应该改为,已经创建好的1000个线程的线程池和协程池可以不需要预备。

--
👇
gwy15: 写了个 (在我看来完全没必要的) benchmark,协程切换上下文的速度在楼主说的“短IO=1ms”的情况下完虐线程。

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=4e7908f8eeb145686711c27bb70e3ea9

Created 10000 coros in 2ms
Finished in 4309ms
Created 10000 threads in 312ms
Finished in 17352ms

反正我这边测试结果是 IO时间短(例如1ms),线程池响应速度快,处理量也更多,时间更短。

那么楼主的结果呢?

作者 zhuxiujia 2020-10-23 14:49
use std::thread::sleep;
use std::time::Duration;
use chrono::Local;


fn use_tps(total: i32, start: i64, end: i64) {
    let mut time = (end - start) as f64;
    time = time / 1000.0;
    println!("use TPS: {} TPS/s", total as f64 / time);
}

//计算每个操作耗时nano纳秒
fn use_time(total: i32, start: i64, end: i64) {
    let mut time = (end - start) as f64;
    time = time / 1000.0;
    println!("use Time: {} s,each:{} nano/op", time, time * 1000000000.0 / (total as f64));
}


fn main() {
    println!("Hello, world!");
    let total = 1000;
    let now = Local::now().timestamp_millis();
    for _ in 0..total {
        //IO time
        sleep(Duration::from_millis(1));
    }
    let end = Local::now().timestamp_millis();
    use_time(total, now, end);
    //use Time: 1.189 s,each:1189000 nano/op
}

#[tokio::test]
async fn tokio_bench() {
    let total = 1000;
    let now = Local::now().timestamp_millis();
    for _ in 0..total {
        //IO time
        tokio::time::delay_for(Duration::from_millis(1)).await;
    }
    let end = Local::now().timestamp_millis();
    use_time(total, now, end);
    //use Time: 2.001 s,each:2001000 nano/op
}

--
👇
gwy15: 你做个减法啊, 协程是 4309-2 = 4307,线程是 17352-312=17040……

--
👇
zhuxiujia: 代码应该改为,已经创建好的1000个线程的线程池和协程池可以不需要预备。

--
👇
gwy15: 写了个 (在我看来完全没必要的) benchmark,协程切换上下文的速度在楼主说的“短IO=1ms”的情况下完虐线程。

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=4e7908f8eeb145686711c27bb70e3ea9

Created 10000 coros in 2ms
Finished in 4309ms
Created 10000 threads in 312ms
Finished in 17352ms

反正我这边测试结果是 IO时间短(例如1ms),线程池响应速度快,处理量也更多,时间更短。

那么楼主的结果呢?

gwy15 2020-10-22 22:40

你做个减法啊, 协程是 4309-2 = 4307,线程是 17352-312=17040……

--
👇
zhuxiujia: 代码应该改为,已经创建好的1000个线程的线程池和协程池可以不需要预备。

--
👇
gwy15: 写了个 (在我看来完全没必要的) benchmark,协程切换上下文的速度在楼主说的“短IO=1ms”的情况下完虐线程。

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=4e7908f8eeb145686711c27bb70e3ea9

Created 10000 coros in 2ms
Finished in 4309ms
Created 10000 threads in 312ms
Finished in 17352ms

反正我这边测试结果是 IO时间短(例如1ms),线程池响应速度快,处理量也更多,时间更短。

那么楼主的结果呢?

作者 zhuxiujia 2020-10-22 22:19

代码应该改为,已经创建好的1000个线程的线程池和协程池可以不需要预备。

--
👇
gwy15: 写了个 (在我看来完全没必要的) benchmark,协程切换上下文的速度在楼主说的“短IO=1ms”的情况下完虐线程。

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=4e7908f8eeb145686711c27bb70e3ea9

Created 10000 coros in 2ms
Finished in 4309ms
Created 10000 threads in 312ms
Finished in 17352ms

反正我这边测试结果是 IO时间短(例如1ms),线程池响应速度快,处理量也更多,时间更短。

那么楼主的结果呢?

fakeshadow 2020-10-22 22:13

异步性能需求高请用mio或者polling。在上面配什么executor和reactor都可以,配多个划分优先级也可以。tokio和async-std主要是方便易用,适合大多数应用场景。

作者 zhuxiujia 2020-10-22 22:13

你这代码创建10000个线程和协程,创建过程肯定线程吃亏呀,因为协程是0开销的,没有申请栈空间

👇
gwy15: 写了个 (在我看来完全没必要的) benchmark,协程切换上下文的速度在楼主说的“短IO=1ms”的情况下完虐线程。

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=4e7908f8eeb145686711c27bb70e3ea9

Created 10000 coros in 2ms
Finished in 4309ms
Created 10000 threads in 312ms
Finished in 17352ms

反正我这边测试结果是 IO时间短(例如1ms),线程池响应速度快,处理量也更多,时间更短。

那么楼主的结果呢?

gwy15 2020-10-22 19:39

写了个 (在我看来完全没必要的) benchmark,协程切换上下文的速度在楼主说的“短IO=1ms”的情况下完虐线程。

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=4e7908f8eeb145686711c27bb70e3ea9

Created 10000 coros in 2ms
Finished in 4309ms
Created 10000 threads in 312ms
Finished in 17352ms

反正我这边测试结果是 IO时间短(例如1ms),线程池响应速度快,处理量也更多,时间更短。

那么楼主的结果呢?

作者 zhuxiujia 2020-10-22 12:57

你可以简单做个benchmark对比,就线程池(最大6线程)和future运行时(最大6线程) 1000个请求 性能对比,反正我这边测试结果是 IO时间短(例如1ms),线程池响应速度快,处理量也更多,时间更短。 如果io时间设置的很长,比如60秒,那么线程池里面,到第6个任务后面全是阻塞了,新IO任务进不来。 而future运行时则把所有的IO请求吃进来了,在上下文切换。

--
👇
Mike Tang: 你确定 IO 短的时候,csp或线程池更高效?

多短?1ms都长了吧?

cutepig123 2020-10-22 12:13

不明白IO的时间如果越短,future的弱点就出来了 有什么原理上的原因?

Mike Tang 2020-10-22 10:58

你确定 IO 短的时候,csp或线程池更高效?

多短?1ms都长了吧?

作者 zhuxiujia 2020-10-21 23:08

比如,你发起一条http请求,耗时5秒这就是io时间长,耗时100ms 这就是耗时短呀

--
👇
Mike Tang: IO时间长短啥意思?

1 2 共 21 条评论, 2 页