Chuyển đến nội dung

std::thread, std::jthread & Quản lý luồng

Tìm hiểu cách tạo và quản lý chủ đề với std::thread và std::jthread của C++20. Hiểu tham gia và tách ra, hủy hợp tác bằng mã thông báo dừng, lưu trữ cục bộ theo luồng và xử lý ngoại lệ trên các ranh giới luồng.

Tại sao đồng thời?

Các CPU hiện đại có nhiều lõi và để khai thác chúng, chương trình của bạn phải chạy đồng thời. C++11 đã giới thiệu thư viện luồng di động trongvà C++20 đã cải tiến thư viện này bằngstd::jthread. luồng là một đường dẫn thực thi độc lập chia sẻ cùng một không gian địa chỉ với luồng đã tạo ra nó - đây vừa là sức mạnh vừa là mối nguy hiểm của nó.

Không giống như các tiến trình, các luồng chia sẻ bộ nhớ. Điều đó giúp giao tiếp nhanh chóng nhưng cũng gây ra cuộc đua dữ liệu nếu hai luồng đọc và ghi cùng một biến mà không đồng bộ hóa. Trước khi giải quyết vấn đề đồng bộ hóa (bài học tiếp theo), trước tiên chúng ta hãy tìm hiểu cách tạo, quản lý và tắt các luồng một cách rõ ràng.

Tạo chủ đề với std::thread

Bạn tạostd::threadbằng cách chuyển cho nó một hàm có thể gọi được (hàm, lambda, functor) và bất kỳ đối số nào. Chuỗi bắt đầu thực thi ngay lập tức khi xây dựng.

create_thread.cpp
#include <iostream>
#include <thread>

void greet(const std::string& name, int times) {
    for (int i = 0; i < times; ++i) {
        std::cout << "Hello, " << name << "! (" << i + 1 << ")\n";
    }
}

int main() {
    // Launch a thread running greet("Alice", 3)
    std::thread t1(greet, "Alice", 3);

    // Lambda-based thread
    std::thread t2([]() {
        std::cout << "Running in a lambda thread\n";
    });

    // MUST join or detach before t1/t2 are destroyed
    t1.join();  // block until t1 finishes
    t2.join();

    std::cout << "All threads finished\n";
    return 0;
}

tham gia () so với tách ()

Mọistd::threadđại diện cho một luồng đang chạy phải được nối hoặc tách rời trước khi hàm hủy của nó chạy. Nếu bạn quên,std::terminate()sẽ được gọi và chương trình của bạn sẽ gặp sự cố.

join() chặn chuỗi cuộc gọi cho đến khi chuỗi đích kết thúc. Đây là mặc định an toàn — bạn biết chuỗi đã hoàn tất và kết quả của nó đã sẵn sàng.

detach() cắt đứt kết nối giữa đối tượngstd::threadvà luồng hệ điều hành. Chuỗi chạy ở chế độ nền ("luồng daemon") và bạn không thể tham gia nó nữa. Hãy thận trọng: nếu luồng tách rời truy cập các biến ngăn xếp từ hàm sinh sản và hàm đó trả về, bạn sẽ nhận được hành vi không xác định.

Lỗi thường gặp

Không bao giờdetach()là một luồng nắm bắt hoặc tham chiếu các biến cục bộ từ phạm vi tạo. Khi hàm tạo trả về, các cục bộ đó sẽ bị hủy và luồng tách ra sẽ truy cập vào bộ nhớ đang treo. Đây là hành vi không xác định và thường biểu hiện dưới dạng các sự cố lẻ tẻ. Nếu bạn phải tách ra, hãy chuyển dữ liệu theo giá trị hoặc sử dụngstd::shared_ptrđể kéo dài thời gian tồn tại.

std::jthread - Chủ đề RAII (C++20)

std::jthreadsolves two problems withstd::thread: (1) it automatically joins in its destructor, so you can never forget, and (2) it supports cooperative cancellation viastd::stop_token.

jthread_demo.cpp
#include <iostream>
#include <thread>
#include <chrono>

void worker(std::stop_token stoken, int id) {
    while (!stoken.stop_requested()) {
        std::cout << "Worker " << id << " running...\n";
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
    }
    std::cout << "Worker " << id << " stopping gracefully\n";
}

int main() {
    std::jthread jt1(worker, 1);  // stop_token is auto-passed as first arg
    std::jthread jt2(worker, 2);

    std::this_thread::sleep_for(std::chrono::seconds(1));

    // Request both threads to stop
    jt1.request_stop();
    jt2.request_stop();

    // No need to call join() — jthread's destructor does it
    std::cout << "Main thread exiting\n";
    return 0;
}

Truy vấn phần cứng & bộ nhớ cục bộ theo luồng

std::thread::hardware_concurrency()trả về số lượng luồng đồng thời mà phần cứng hỗ trợ. Thời lượng lưu trữthread_localcung cấp cho mỗi luồng một bản sao biến riêng.

hardware_tls.cpp
#include <iostream>
#include <thread>
#include <vector>

thread_local int tls_counter = 0;  // each thread gets its own copy

void count_work(int id) {
    for (int i = 0; i < 1000; ++i) {
        ++tls_counter;  // no data race — each thread has its own
    }
    std::cout << "Thread " << id << ": tls_counter = " << tls_counter << "\n";
}

int main() {
    unsigned int n = std::thread::hardware_concurrency();
    std::cout << "Hardware concurrency: " << n << " threads\n";

    std::vector<std::thread> threads;
    for (unsigned int i = 0; i < n; ++i) {
        threads.emplace_back(count_work, i);
    }
    for (auto& t : threads) {
        t.join();
    }
    // Each thread printed 1000 — they don't share tls_counter
    return 0;
}
Thực hành tốt

In C++20 and later, preferstd::jthreadoverstd::thread. It automatically joins in its destructor, eliminating the risk of callingstd::terminateif you forget to join. Its built-instd::stop_tokenmechanism provides a clean, standardized way to request cooperative cancellation — no need to roll your ownatomic shouldStopflag.

Xử lý ngoại lệ trên các chủ đề

Một ngoại lệ được ném vào bên trong một chuỗi không bị bắt trong chuỗi đó gọistd::terminate(). Các ngoại lệ không tự động lan truyền đến chuỗi tham gia.

Để chuyển ngoại lệ giữa các luồng, hãy ghi lại chúng bằngstd::current_exception()bên trong luồng công việc và lưu trữ chúng trongstd::exception_ptr. Sau đó, chuỗi tham gia có thể được nối lại bằngstd::rethrow_exception(). Cơ chếstd::future(được trình bày trong Bài 4) tự động xử lý việc này.

Điểm chính
  • std::threadbắt đầu thực thi ngay lập tức; bạn phảijoin()hoặcdetach()trước khi hàm hủy của nó chạy
  • std::jthread(C++20) tự động tham gia vào hàm hủy của nó và hỗ trợ hủy hợp tác thông quastd::stop_token
  • thread_localcung cấp cho mỗi luồng bản sao biến độc lập của riêng nó - không cần đồng bộ hóa
  • std::thread::hardware_concurrency()cho bạn biết phần cứng có thể chạy đồng thời bao nhiêu luồng
  • Các ngoại lệ trong một chuỗi không truyền tới người gọi - sử dụngstd::exception_ptrhoặcstd::futuređể chuyển chúng
  • Không bao giờ để một luồng tách rời tham chiếu các biến cục bộ từ phạm vi sinh sản

Câu đố — Kiểm tra kiến thức

(15 XP)

1. Điều gì xảy ra nếu một đối tượng`std::thread`đại diện cho một luồng có thể nối bị hủy mà không gọi`join()`hoặc`detach()`?

2. `std::jthread`hỗ trợ việc hủy hợp tác như thế nào?

3. Từ khóa`thread_local`làm gì?