Sửa lỗi Rust: 'future cannot be sent between threads safely' trong tokio::spawn

intermediate🦀 Rust2026-04-22| Rust (mọi phiên bản), Tokio runtime, Multi-threaded executor

Error Message

error[E0277]: future cannot be sent between threads safely, the trait `Send` is not implemented
#rust#async#tokio#Send#Future#luồng

Thông báo lỗi

Trình biên dịch của Rust nổi tiếng là hữu ích, nhưng lỗi E0277 vẫn có thể giống như một bức tường gạch khi bạn mới gặp lần đầu. Bạn có lẽ đang cố gắng tạo một tác vụ nền (background task), nhưng lại nhận về một loạt chữ đỏ thông báo rằng future is not Send. Nó thường trông như thế này:

error[E0277]: future cannot be sent between threads safely
   --> src/main.rs:10:5
    |
10  |     tokio::spawn(async move {
    |     ^^^^^^^^^^^^ future returned by `async` block is not `Send` 
    |
    = help: the trait `Send` is not implemented for `std::sync::MutexGuard<i32>`
    = note: future is not `Send` as this value is used across an await

Tại sao lỗi này xảy ra

Bộ lập lịch mặc định của Tokio là một hệ thống work-stealer đa luồng. Để giữ cho các nhân CPU luôn bận rộn, nó di chuyển các tác vụ giữa một nhóm luồng làm việc (worker threads)—thường tương ứng với số lượng nhân của bạn. Một tác vụ có thể bắt đầu trên Luồng 1, gặp một điểm .await, sau đó tiếp tục trên Luồng 4 khi dữ liệu đã sẵn sàng.

Việc "nhảy luồng" này mang lại hiệu suất cao, nhưng nó tạo ra một yêu cầu khắt khe: mọi dữ liệu được giữ qua điểm .await đó phải triển khai (implement) trait Send. Nếu bạn giữ một kiểu dữ liệu không an toàn cho luồng (non-thread-safe) trong khi tác vụ đang tạm dừng, trình biên dịch sẽ ngăn bạn tạo ra tình trạng tranh chấp dữ liệu (data race).

Hầu hết các lập trình viên gặp lỗi này vì hai lý do:

  • Tranh chấp khóa: Giữ một std::sync::MutexGuard tiêu chuẩn trong khi gọi .await.
  • Sử dụng sai kiểu nguyên thủy: Sử dụng các kiểu đơn luồng như Rc hoặc RefCell bên trong một khối async.

Giải pháp 1: Giải phóng Guard sớm

std::sync::MutexGuard không phải là Send vì nó dựa trên ID luồng ở cấp hệ điều hành, vốn không thể chuyển giao giữa các luồng. Nếu logic của bạn chỉ cần khóa để cập nhật nhanh, hãy đảm bảo guard được hủy trước khi bạn đến một điểm .await.

Mã lỗi:

use std::sync::Mutex;

async fn problematic_task(data: Arc<Mutex<i32>>) {
    tokio::spawn(async move {
        let mut guard = data.lock().unwrap();
        *guard += 1;
        
        // Lệnh await này giữ cho guard tiếp tục tồn tại giữa các luồng!
        some_async_function().await; 
        
        println!("Value: {}", *guard);
    });
}

Mã đã sửa:

use std::sync::Mutex;

async fn fixed_task(data: Arc<Mutex<i32>>) {
    tokio::spawn(async move {
        {
            let mut guard = data.lock().unwrap();
            *guard += 1;
        } // Guard được giải phóng tại đây, cho phép tác vụ di chuyển sang luồng khác

        some_async_function().await;
    });
}

Việc bao bọc logic mutex trong một khối đơn giản { ... } đảm bảo MutexGuard ra khỏi phạm vi (out of scope). Đây là cách sửa lỗi hiệu quả nhất vì nó tránh được chi phí của các khóa async phức tạp hơn.

Giải pháp 2: Chuyển sang Mutex của Tokio

Đôi khi bạn thực sự cần giữ một khóa trong khi chờ phản hồi API hoặc truy vấn cơ sở dữ liệu. Trong những trường hợp này, hãy thay thế Mutex của thư viện tiêu chuẩn bằng tokio::sync::Mutex. Guard của nó được thiết kế đặc biệt để có tính Send.

Cách triển khai:

use tokio::sync::Mutex;
use std::sync::Arc;

async fn holding_lock_across_await(data: Arc<Mutex<i32>>) {
    tokio::spawn(async move {
        // Lưu ý: .lock() bây giờ là một lời gọi async
        let mut guard = data.lock().await; 
        
        // Điều này an toàn vì tokio::sync::MutexGuard triển khai Send
        some_async_function().await;
        
        *guard += 1;
    });
}

Hãy sử dụng điều này một cách tiết chế. Giữ một khóa qua một điểm .await có thể gây thắt nút cổ chai cho ứng dụng của bạn nếu hàm async đó mất nhiều thời gian để hoàn thành.

Giải pháp 3: Thay thế Rc bằng Arc

Nếu thông báo lỗi của bạn đề cập đến std::rc::Rc, bạn đang sử dụng một bộ đếm tham chiếu không có tính nguyên tử (atomic). Rc nhanh hơn cho các công việc đơn luồng nhưng thiếu sự đồng bộ hóa nội bộ cần thiết cho các nhóm luồng. Bạn phải sử dụng Arc (Atomic Reference Counted) thay thế.

Lỗi:

let shared_val = Rc::new(10);
tokio::spawn(async move {
    println!("{}", shared_val); // Rc không có tính Send
});

Đã sửa:

let shared_val = Arc::new(10);
tokio::spawn(async move {
    println!("{}", shared_val); // Arc an toàn cho luồng và có tính Send
});

Các bước kiểm tra

Chạy kiểm tra nhanh để xem trình biên dịch đã hài lòng chưa:

cargo check

Nếu lỗi vẫn còn, hãy nhìn kỹ vào phần "note" của kết quả đầu ra. Nó thường chỉ ra chính xác dòng mà một biến non-Send được tạo ra. Hãy tìm các cụm từ như "this value is used across an await" để tìm ra thủ phạm.

Lời khuyên thực hành tốt nhất

  • Kiểm tra sớm: Chạy cargo check thường xuyên thay vì đợi cho đến khi bạn có hàng trăm dòng mã.
  • Giữ các phần nhỏ gọn: Hạn chế các phần quan trọng (critical sections) của bạn chỉ trong vài dòng mã bất cứ khi nào có thể.
  • Tránh RefCell: Nếu bạn cần khả năng thay đổi nội tại (interior mutability) giữa các luồng, hãy sử dụng Mutex hoặc RwLock.
  • Xác minh trait tùy chỉnh: Nếu tác vụ của bạn liên quan đến một trait tùy chỉnh, hãy đảm bảo nó có ràng buộc + Send để Tokio có thể di chuyển nó giữa các worker.

Related Error Notes