Fix lỗi Rust Tokio 'Cannot start a runtime from within a runtime' Khi Gọi block_on Trong Async Context

intermediate🦀 Rust2026-07-10| Rust 1.60+, Tokio 1.x, Linux/macOS/Windows — mọi nền tảng chạy Tokio async runtime

Error Message

thread 'main' panicked at 'Cannot start a runtime from within a runtime. This happens because a function (like `block_on`) attempted to block the current thread while the thread is being used to drive asynchronous tasks.'
#tokio#async#runtime#block_on#nested-runtime

TL;DR

Tokio không cho phép lồng runtime. Gọi block_on() từ bên trong một hàm async đang chạy trên các thread của Tokio sẽ gây panic ngay lập tức. Cách sửa phù hợp tùy thuộc vào ai sở hữu đoạn code có vấn đề:

  • Bạn sở hữu code async? Thay block_on(future) bằng future.await. Xong.
  • Đang gọi một thư viện sync có block_on bên trong? Chuyển lời gọi vào tokio::task::spawn_blocking hoặc tokio::task::block_in_place.
  • Cần cô lập runtime hoàn toàn? Tạo một OS thread thực sự và cấp cho nó Runtime::new() riêng.

Điều Gì Đã Thực Sự Xảy Ra

Các thread trong thread pool của Tokio thuộc sở hữu của runtime. Gọi Handle::block_on() hoặc Runtime::block_on() từ một trong những thread đó và Tokio sẽ panic có chủ đích — chặn một thread đang poll future sẽ khiến executor bị deadlock ngay lập tức.

Ba tình huống gây ra lỗi này nhiều hơn bất cứ điều gì khác:

  • Một thư viện bên thứ ba gọi block_on bên trong. SDK wrapper bao quanh các async HTTP client thường là thủ phạm.
  • Một helper sync xây dựng Runtime cục bộ và gọi block_on, sau đó bạn gọi helper đó từ bên trong một async fn.
  • Bạn đặt #[tokio::main] trên main(), rồi cũng thử tokio::runtime::Builder::new_current_thread().block_on() ở đâu đó trong phần thân hàm.
// Panic — block_on bên trong ngữ cảnh #[tokio::main]
#[tokio::main]
async fn main() {
    let rt = tokio::runtime::Runtime::new().unwrap();
    let result = rt.block_on(some_async_fn()); // PANIC tại đây
    println!("{:?}", result);
}

Cách Sửa 1: Dùng .await (Lựa Chọn Mặc Định Đúng Đắn)

Bạn kiểm soát code? Bỏ hoàn toàn block_on. Để .await làm nhiệm vụ của nó.

// Trước (bị panic)
fn fetch_data() -> String {
    let rt = tokio::runtime::Runtime::new().unwrap();
    rt.block_on(async_fetch()) // panic bên trong ngữ cảnh async
}

// Sau (đúng)
async fn fetch_data() -> String {
    async_fetch().await
}

Khi đó nơi gọi sẽ trở thành:

#[tokio::main]
async fn main() {
    let data = fetch_data().await;
    println!("{}", data);
}

Đây hầu như luôn là câu trả lời đúng. Chỉ dùng các cách sửa khác khi một thư viện bắt buộc bạn phải làm vậy.

Cách Sửa 2: spawn_blocking cho Công Việc Sync hoặc Tốn CPU

Có một hàm sync buộc phải block — một SDK cũ, một tính toán nặng CPU, I/O file đồng bộ? Chuyển nó sang thread pool blocking chuyên dụng của Tokio bằng spawn_blocking. Những thread này được quản lý tách biệt khỏi async executor. Không panic, không deadlock.

#[tokio::main]
async fn main() {
    let result = tokio::task::spawn_blocking(|| {
        // An toàn để block tại đây — chạy trên một blocking thread chuyên dụng
        legacy_sync_client::fetch()
    })
    .await
    .unwrap();

    println!("{:?}", result);
}

Tokio giới hạn blocking thread ở mức 512 theo mặc định. Con số đó khá rộng rãi cho các lần gọi thỉnh thoảng. Nếu bạn spawn hàng trăm cái dưới tải liên tục, hãy tinh chỉnh giới hạn bằng Builder::max_blocking_threads() trước khi nó ảnh hưởng đến môi trường production.

Cách Sửa 3: block_in_place cho Block Nội Tuyến

Thay vì chuyển sang thread khác, block_in_place di chuyển các task khác ra khỏi thread hiện tại để bạn có thể block ngay tại chỗ. Dễ kết nối hơn spawn_blocking — nhưng chỉ hoạt động trên runtime multi-thread.

#[tokio::main] // mặc định = multi_thread
async fn main() {
    let result = tokio::task::block_in_place(|| {
        // Lời gọi blocking nội tuyến — runtime di chuyển các task khác ra khỏi thread này
        some_blocking_sdk::run()
    });
    println!("{:?}", result);
}

Đang chạy trên current_thread? Dùng spawn_blocking thay thế. block_in_place sẽ panic trên runtime single-thread vì không có thread nào khác để di chuyển task sang.

Cách Sửa 4: OS Thread Riêng Biệt với Runtime Của Chính Nó

Một số thư viện gọi block_on nội bộ và bạn thực sự không thể thay đổi chúng. Cách giải quyết: đừng chạy chúng trên thread của Tokio. Spawn một OS thread thực sự — hoàn toàn bên ngoài executor — và xây dựng một runtime mới ở đó.

#[tokio::main]
async fn main() {
    // Một OS thread thực sự, không phải Tokio task
    let handle = std::thread::spawn(|| {
        let rt = tokio::runtime::Runtime::new().unwrap();
        rt.block_on(async {
            // An toàn — runtime mới trên thread mà Tokio không sở hữu
            some_library_with_internal_block_on()
        })
    });

    let result = handle.join().unwrap();
    println!("{:?}", result);
}

Hơi cồng kềnh? Một chút. Nhưng khi một dependency hard-code block_on và bạn không thể chạm vào nó, đây là cách thoát đáng tin cậy nhất.

Chẩn Đoán Cách Sửa Bạn Cần

Kiểm tra stack trace của panic. Nó chỉ thẳng vào thủ phạm:

thread 'tokio-runtime-worker' panicked at 'Cannot start a runtime from within a runtime.
...
stack backtrace:
   0: tokio::runtime::scheduler::block_on
   1: some_sdk::client::Client::send   <-- thủ phạm
   2: my_crate::handler::process
   3: tokio::runtime::task::harness

Code thư viện ở frame 1? Bạn cần Cách Sửa 2 hoặc 4. Code của bạn ở frame 1? Cách Sửa 1 hoặc 3.

Đang viết một thư viện phải xử lý cả caller sync lẫn async? Dùng Handle::try_current() để phát hiện tình huống lúc runtime:

pub fn do_work() {
    match tokio::runtime::Handle::try_current() {
        Ok(_handle) => {
            // Đang ở trong Tokio — caller phải dùng do_work_async().await thay thế
            panic!("do_work() không thể gọi từ ngữ cảnh async; hãy dùng do_work_async().await");
        }
        Err(_) => {
            // Không có runtime — an toàn để tạo mới
            let rt = tokio::runtime::Runtime::new().unwrap();
            rt.block_on(internal_async_work());
        }
    }
}

Xác Nhận Đã Sửa Xong

Chạy binary và xác nhận panic đã biến mất:

cargo run 2>&1 | grep -i "cannot start a runtime"
# Không có output = đã sửa xong

Thêm integration test để phát hiện regression sớm:

#[tokio::test]
async fn test_no_nested_runtime_panic() {
    // Không được panic
    let result = fetch_data().await;
    assert!(!result.is_empty());
}

Dùng spawn_blocking dưới tải thực tế? Hãy theo dõi số lượng thread. Giới hạn mặc định 512 thread nghe có vẻ rộng rãi, nhưng các lời gọi blocking liên tục tích lũy nhanh. Kiểm tra mức sử dụng với Builder::max_blocking_threads() trước khi bạn chạm trần trong môi trường production.

Tham Khảo Nhanh

  • .await — lựa chọn đầu tiên khi bạn sở hữu code
  • spawn_blocking — code sync trên blocking thread; hoạt động trên cả runtime multi-thread lẫn current_thread
  • block_in_place — block nội tuyến; chỉ dành cho runtime multi-thread
  • std::thread::spawn + Runtime mới — khi dependency bắt buộc dùng block_on và không thể sửa đổi

Related Error Notes