5

我有一个使用标准库的简单 TCP 回显服务器:

use std::net::TcpListener;

fn main() {
    let listener = TcpListener::bind("localhost:4321").unwrap();
    loop {
        let (conn, _addr) = listener.accept().unwrap();
        std::io::copy(&mut &conn, &mut &conn).unwrap();
    }
}

它使用大约 11 MB 的内存:

标准库

东京

如果我将其转换为使用 tokio:

tokio = { version = "0.2.22", features = ["full"] }
use tokio::net::TcpListener;

#[tokio::main]
async fn main() {
    let mut listener = TcpListener::bind("localhost:4321").await.unwrap();
    loop {
        let (mut conn, _addr) = listener.accept().await.unwrap();
        let (read, write) = &mut conn.split();
        tokio::io::copy(read, write).await.unwrap();
    }
}

它使用 607 MB 内存:

东京

async_std

同样,使用 async_std:

async-std = "1.6.2"
use async_std::net::TcpListener;

fn main() {
    async_std::task::block_on(async {
        let listener = TcpListener::bind("localhost:4321").await.unwrap();
        loop {
            let (conn, _addr) = listener.accept().await.unwrap();
            async_std::io::copy(&mut &conn, &mut &conn).await.unwrap();
        }
    });
}

它还使用 607 MB 内存:

async_std


为什么程序的异步版本使用的内存是同步版本的 55 倍?

4

3 回答 3

5

你应该看看RES专栏。一个使用 1.0MB,另一个使用 1.6MB。

其中大部分可能是启动 tokio 运行时和为其创建线程池所需的持续开销。

于 2020-08-04T14:22:52.623 回答
3

我在这里试过了,就像你在评论中说的那样,有几个 64MB 块:

==> pmap -d $(pidof tokio)
3605:   target/release/tokio
Address           Kbytes Mode  Offset           Device    Mapping
…
0000555b2a634000     132 rw--- 0000000000000000 000:00000   [ anon ]
00007f2fec000000     132 rw--- 0000000000000000 000:00000   [ anon ]
00007f2fec021000   65404 ----- 0000000000000000 000:00000   [ anon ]
00007f2ff0000000     132 rw--- 0000000000000000 000:00000   [ anon ]
00007f2ff0021000   65404 ----- 0000000000000000 000:00000   [ anon ]
00007f2ff4000000     132 rw--- 0000000000000000 000:00000   [ anon ]
00007f2ff4021000   65404 ----- 0000000000000000 000:00000   [ anon ]
…

这些块既不可读也不可写,因此它们没有被映射并且不使用任何内存。它们只是代表保留的地址空间。

此外,如您所见,每个 65404K 块都紧跟在 132K 块之后。由于 65404+132 正好是 65536,我怀疑这些块代表保留的地址空间,以防运行时需要在以后增长那些 132K 块之一。看看几个小时和几千个连接后的情况可能会很有趣。

于 2020-08-05T07:14:33.233 回答
1

glibc 的 malloc 实现为每个线程分配一个新块。块的大小由编译时常量 HEAP_MAX_SIZE( Source ) 指定。因为 tokio 运行时会产生多个线程,所以会导致虚拟内存使用率很高。

为了避免这种情况,你可以为 musl 目标编译你的 rust 程序cargo build --target=x86_64-unknown-linux-musl

毕竟,这是 glibc 的优化,而不是 rust 或 tokio 运行时的影响。

于 2021-06-28T11:41:05.693 回答