现代C++中的并发编程:从线程到协程

C++20 引入了多种工具,使得并发编程变得更直观、更加安全。本文从标准库提供的 std::threadstd::asyncstd::future 等基本构件谈起,逐步过渡到更高级的 std::jthread、协程(std::coroutine)以及与第三方库的结合,帮助读者快速掌握现代并发技术的核心思想与实战技巧。

1. 基础线程与同步

1.1 std::thread 的基本使用

#include <thread>
#include <iostream>

void worker(int id) {
    std::cout << "Thread " << id << " started\n";
    // 模拟耗时操作
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Thread " << id << " finished\n";
}

int main() {
    std::thread t1(worker, 1);
    std::thread t2(worker, 2);
    t1.join();
    t2.join();
}

std::thread 提供了最直接的线程创建方式,但它的生命周期管理不够友好。若忘记 join()detach(),程序会抛出异常。

1.2 互斥与条件变量

#include <mutex>
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void producer() {
    std::unique_lock<std::mutex> lock(mtx);
    ready = true;
    cv.notify_one();
}

void consumer() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, []{ return ready; });
    // 处理数据
}

使用 std::unique_lockstd::condition_variable 可以实现线程间的同步与等待。

2. 任务包装与异步执行

2.1 std::asyncstd::future

std::async 可以在后台启动任务,并返回一个 std::future 供主线程获取结果。默认情况下,std::async 的策略是 launch::asynclaunch::deferred,可通过显式参数指定。

#include <future>
#include <numeric>

int main() {
    auto fut = std::async(std::launch::async, std::accumulate, std::begin(nums), std::end(nums), 0);
    // 继续做别的事
    int sum = fut.get();  // 这里会等待结果
}

2.2 std::packaged_taskstd::promise

这两者可以将函数包装成可被异步执行的任务,或者手动控制结果的设置与获取。

std::packaged_task<int(int, int)> task([](int a, int b){ return a + b; });
std::future <int> result = task.get_future();
std::thread(std::move(task), 2, 3).detach(); // 在新线程中执行

3. C++20 的 std::jthread:更安全的线程

std::jthreadstd::thread 的基础上增加了自动停止功能,线程对象在析构时会尝试停止其执行。其构造函数接受一个 stop_token,可以让线程内部及时响应停止请求。

#include <jthread>
#include <iostream>

void worker(std::stop_token st) {
    while (!st.stop_requested()) {
        std::cout << "Working...\n";
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
    }
    std::cout << "Stopped\n";
}

int main() {
    std::jthread t(worker);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    t.request_stop();  // 通知线程停止
}

std::jthread 在多线程程序中极大减少了资源泄露的风险,推荐在现代 C++ 代码中使用。

4. 协程(std::coroutine)的引入

4.1 协程基础概念

协程是一种 轻量级 的线程切换方式,函数执行可以被挂起(co_awaitco_yield)并恢复,极大简化异步编程模型。

#include <coroutine>
#include <iostream>

struct Generator {
    struct promise_type {
        int current_value;
        std::suspend_always yield_value(int value) {
            current_value = value;
            return {};
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        Generator get_return_object() { return {}; }
        void unhandled_exception() {}
        void return_void() {}
    };

    struct iterator {
        Generator* g;
        bool operator!=(std::default_sentinel) { return true; }
        int operator*() { return g->promise().current_value; }
        iterator& operator++() { g->promise().yield_value(g->promise().current_value + 1); return *this; }
    };

    iterator begin() { return {this}; }
    std::default_sentinel end() { return {}; }
};

4.2 实战示例:异步文件读取

在 C++20 之前,异步文件 I/O 通常需要回调或线程池。协程可以让异步读取像同步代码一样直观。

#include <filesystem>
#include <fstream>
#include <coroutine>
#include <string>

struct async_file_reader {
    struct promise_type {
        std::string data;
        std::string filename;
        async_file_reader get_return_object() { return {}; }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void unhandled_exception() { throw; }
        void return_void() {}
        void yield_value(std::string&& chunk) { data += std::move(chunk); }
    };

    struct coroutine_handle_t {
        std::coroutine_handle <promise_type> h;
    };

    static coroutine_handle_t read(std::string path) {
        auto co = async_file_reader{path};
        // ...
    }
};

(此处省略完整实现,重点是展示协程的使用方式)

5. 与第三方库的协作

5.1 ThreadPool(Boost.Asio

Boost.Asio 提供了可跨平台的线程池与异步任务调度器。通过 io_context 可以轻松管理多个任务。

boost::asio::io_context io;
boost::asio::thread_pool pool(4);

boost::asio::post(pool, []{
    // 任务内容
});
pool.join();

5.2 TBB(Threading Building Blocks)

TBB 的 parallel_for, parallel_reduce, task_group 等抽象可以让并行算法以声明式方式编写。

tbb::parallel_for(tbb::blocked_range <int>(0, N), [&](const tbb::blocked_range<int>& r){
    for (int i=r.begin(); i!=r.end(); ++i) {
        data[i] = heavy_computation(i);
    }
});

6. 并发编程的最佳实践

  1. 避免数据竞争:使用 std::mutexstd::shared_mutexstd::atomic
  2. 最小化锁粒度:只在必要时持有锁,减少阻塞。
  3. 使用 RAII 管理资源std::lock_guardstd::scoped_lock
  4. 优先考虑线程池:减少频繁创建销毁线程的开销。
  5. 充分利用协程:降低线程开销,简化异步逻辑。

7. 小结

C++ 从最初的 std::thread 到现在的 std::jthread 与协程,已经形成了完整而强大的并发编程生态。通过结合标准库与成熟的第三方库,程序员可以在保持代码可读性和安全性的前提下,构建高效、可伸缩的并行应用。掌握这些工具与思想,能让你在面对大规模并发任务时游刃有余。

发表评论