如何配置多线程与 current_thread 运行时?

解读

国内后端/基础架构面试中,异步运行时选型是 Rust 岗位的高频考点。面试官不仅想看你会不会写 tokio::spawn,更关心你对 运行时线程模型 的理解:

  1. 什么时候用 多线程(multi-thread) 运行时?
  2. 什么时候用 current_thread 单线程运行时?
  3. 如何通过 Cargo features代码 显式切换?
  4. 两种模型对 阻塞调用、CPU 密集型任务、延迟敏感型服务 的影响?
    答不到“编译期特征开关 + 运行时 Builder 模式”这一层,很容易被追问到“为什么你的服务在 4 核容器里只跑满 1 核”。

知识点

  1. tokio 运行时两种线程模型

    • multi_thread:默认,工作窃取调度器,线程数 = CPU 核数,可 scale 到 100% 多核。
    • current_thread:全部 Task 挂在当前线程,无 cross-thread 调度,无锁开销,延迟极低,但无法利用多核。
  2. Cargo.toml 特征开关

    [dependencies]
    tokio = { version = "1", features = ["full"] }        # 默认启用 multi_thread
    tokio = { version = "1", features = ["rt", "macros"] } # 只启用 current_thread
    

    国内镜像源(清华/中科大)同步延迟低,features 写错会直接编译失败,面试官会看日志排错能力。

  3. 运行时 Builder 显式指定

    let rt = tokio::runtime::Builder::new_multi_thread()
        .worker_threads(4)          // 国内 4 核容器常见
        .max_blocking_threads(512)  // 兼容同步 JDBC/Redis 客户端
        .enable_all()
        .build()?;
    

    对比

    let rt = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()?;
    
  4. #[tokio::main] 宏展开
    宏默认使用 new_multi_thread();想写单测或嵌入式场景,需手动写 #[tokio::main(flavor = "current_thread")]

  5. 阻塞代码迁移
    多线程下必须用 spawn_blockingmysql::conn、reqwest::blocking 等丢到独立线程池;
    current_thread 下若直接 std::thread::sleep卡住整个事件循环,导致 QPS 掉零,面试时必被追问“怎么定位”。

  6. 统计与调试
    国内生产环境常接 阿里云 SLS / 腾讯云 CLS,通过 tokio-console 需打开 console feature,
    多线程运行时能看到 任务在不同 worker 间窃取;current_thread 只有一条主线程,无窃取事件。

答案

“配置”分三步:依赖声明、运行时 Builder、阻塞策略

  1. 依赖层:
    多线程——tokio = { version = "1", features = ["full"] }
    current_thread——tokio = { version = "1", features = ["rt", "macros"] }
  2. 代码层:
    多线程
    let rt = tokio::runtime::Builder::new_multi_thread()
        .worker_threads(8)           // 按容器核数设
        .thread_stack_size(2 * 1024 * 1024) // 国内 64 位云主机默认 8 MB,可调小省内存
        .max_blocking_threads(200)   // 兼容老版 Oracle 驱动
        .enable_all()
        .build()?;
    rt.block_on(async_main())
    
    current_thread
    let rt = tokio::runtime::Builder::new_current_thread()
        .enable_io()
        .enable_time()
        .build()?;
    rt.block_on(async_main())
    
  3. 阻塞策略:
    多线程里遇到 sync 接口 立即 tokio::task::spawn_blocking(move || { heavy_work() })
    current_thread 里禁止任何阻塞 syscall,否则整个 reactor 被挂起,延迟飙升。

一句话总结:用 Cargo features 选“轮子”,用 Builder 选“引擎”,用 spawn_blocking 保“不堵”

拓展思考

  1. 国内 信创 ARM 服务器 核数高达 128,tokio 默认线程数 = CPU 核,惊群效应调度开销 如何权衡?
    答:通过 .worker_threads(32) 手动封顶,再配 numa_affinity 把线程绑核,降低跨 Die 切换。

  2. WebAssembly + current_thread 在浏览器主线程运行,如何避免 长时间计算 冻结页面?
    答:使用 wasm-bindgen-futures 把 Task 切成 16 ms 以下切片,或 postMessage 到 Web Worker 模拟多线程。

  3. 面试常问“为什么 async-std 没有 current_thread 概念”?
    答:async-std 默认就是全局线程池,单线程需求靠自建 LocalSet;tokio 把两种模型都暴露给开发者,选型灵活但学习成本高,正是 Rust 面试区分度所在。