C++ 线程与协程的选择指南:从场景到实践



一、引言:并发编程的两种范式


线程(Thread)和协程(Coroutine)是 C++ 中实现并发的两种核心机制。线程由操作系统调度,是 CPU 调度的最小单位;协程由程序自身调度,是用户态的“轻量级线程”。选择错误的工具会导致性能瓶颈、资源浪费或代码复杂度失控。


理解两者的本质差异和适用边界,是构建高性能 C++ 应用的基础。



二、核心区别速览



| 维度 | 线程 (std::thread) | 协程 (C++20 Coroutine) |

| --- | --- | --- |

| 调度者 | 操作系统内核(抢占式) | 用户代码/事件循环(协作式) |

| 切换开销 | 高(系统调用、上下文切换,~1-10μs) | 极低(函数调用级别,~10-100ns) |

| 内存开销 | 大(每线程独立栈,通常 1-8MB) | 极小(仅堆上协程帧,通常几十到几百字节) |

| 并发数 | 有限(数百到数千) | 极高(数万到数百万) |

| 并行性 | 真正并行(多核同时执行) | 单线程内并发,非并行 |

| 阻塞操作 | 线程阻塞不影响其他线程 | 协程阻塞会阻塞整个线程 |

| 编程模型 | 复杂(锁、同步、数据竞争) | 相对简单(无锁协作式) |

| 适用场景 | CPU 密集型、阻塞 I/O、需要并行 | 高并发 I/O、非阻塞异步、生成器 |



三、使用线程的场景



3.1 CPU 密集型计算


线程是唯一能实现真正并行计算的方式,可以利用多核 CPU 同时执行。


cpp复制下载

#include <thread>
#include <vector>
#include <numeric>
#include <iostream>

// CPU 密集型:矩阵乘法、图像处理、科学计算
void parallelSum(const std::vector<int>& data, 
                 size_t start, size_t end, long long& result) {
    result = std::accumulate(data.begin() + start, data.begin() + end, 0LL);
}

int main() {
    std::vector<int> data(10'000'000, 1);
    long long sum1 = 0, sum2 = 0, sum3 = 0, sum4 = 0;
    
    // 利用 4 核并行计算
    size_t chunk = data.size() / 4;
    std::thread t1(parallelSum, std::ref(data), 0, chunk, std::ref(sum1));
    std::thread t2(parallelSum, std::ref(data), chunk, 2*chunk, std::ref(sum2));
    std::thread t3(parallelSum, std::ref(data), 2*chunk, 3*chunk, std::ref(sum3));
    std::thread t4(parallelSum, std::ref(data), 3*chunk, data.size(), std::ref(sum4));
    
    t1.join(); t2.join(); t3.join(); t4.join();
    std::cout << "Total: " << sum1 + sum2 + sum3 + sum4 << std::endl;
}



3.2 阻塞式系统调用


当操作涉及阻塞式 I/O(如读文件、等待用户输入),线程可以在阻塞时让出 CPU。


cpp复制下载

// 阻塞 I/O:文件读写、socket 阻塞模式、数据库查询
void blockingFileRead(const std::string& filename) {
    std::ifstream file(filename, std::ios::binary);
    std::vector<char> buffer(1024 * 1024); // 1MB
    file.read(buffer.data(), buffer.size()); // 阻塞操作,但不影响其他线程
    // 处理数据...
}

std::thread readerThread(blockingFileRead, "large_file.bin");
// 主线程可以做其他事情
doOtherWork();
readerThread.join();



3.3 需要与现有同步模型集成


cpp复制下载

// 使用 condition_variable 实现生产者-消费者
class ThreadSafeQueue {
    std::queue<int> queue_;
    mutable std::mutex mtx_;
    std::condition_variable cv_;
    
public:
    void push(int value) {
        std::lock_guard lock(mtx_);
        queue_.push(value);
        cv_.notify_one();  // 唤醒等待的线程
    }
    
    int pop() {
        std::unique_lock lock(mtx_);
        cv_.wait(lock, [this] { return !queue_.empty(); });
        int value = queue_.front();
        queue_.pop();
        return value;
    }
};



四、使用协程的场景



4.1 高并发 I/O 密集型服务


协程最适合处理大量并发连接的网络服务,避免为每个连接创建线程。


cpp复制下载

#include <boost/asio.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/co_spawn.hpp>

using boost::asio::awaitable;
using boost::asio::co_spawn;
using boost::asio::use_awaitable;

// 协程处理单个客户端连接
awaitable<void> handleClient(tcp::socket socket) {
    try {
        char buffer[1024];
        while (true) {
            // 异步读取,不阻塞线程
            size_t n = co_await socket.async_read_some(
                boost::asio::buffer(buffer), use_awaitable);
            // 异步写入
            co_await async_write(socket, 
                boost::asio::buffer(buffer, n), use_awaitable);
        }
    } catch (const std::exception& e) {
        std::cerr << "Client error: " << e.what() << std::endl;
    }
}

// 主协程:接受连接
awaitable<void> listener(tcp::acceptor& acceptor) {
    while (true) {
        auto socket = co_await acceptor.async_accept(use_awaitable);
        co_spawn(acceptor.get_executor(), 
                 handleClient(std::move(socket)), detached);
    }
}

int main() {
    boost::asio::io_context ioContext;
    tcp::acceptor acceptor(ioContext, tcp::endpoint(tcp::v4(), 8080));
    co_spawn(ioContext, listener(acceptor), detached);
    ioContext.run();  // 事件循环驱动所有协程
}


性能对比:在 10 万并发连接场景下,使用线程可能需要 10 万个线程(每个 8MB 栈 = 800GB 内存),而协程仅需 10 万个协程帧(每个约 100 字节 = 10MB 内存)。



4.2 生成器和惰性求值


协程可以自然地表达惰性序列,按需产生值而非一次性计算全部结果。


cpp复制下载

#include <coroutine>
#include <iostream>
#include <optional>

// 生成器:按需产生斐波那契数列
template<typename T>
struct Generator {
    struct promise_type {
        T currentValue;
        auto get_return_object() { return Generator{this}; }
        auto initial_suspend() { return std::suspend_always{}; }
        auto final_suspend() noexcept { return std::suspend_always{}; }
        auto yield_value(T value) {
            currentValue = value;
            return std::suspend_always{};
        }
        void return_void() { }
        void unhandled_exception() { std::terminate(); }
    };
    
    struct iterator {
        std::coroutine_handle<promise_type> handle;
        bool operator!=(std::default_sentinel_t) const { return !handle.done(); }
        iterator& operator++() { handle.resume(); return *this; }
        T operator*() const { return handle.promise().currentValue; }
    };
    
    iterator begin() { handle.resume(); return {handle}; }
    std::default_sentinel_t end() { return {}; }
    
private:
    explicit Generator(promise_type* p) 
        : handle(std::coroutine_handle<promise_type>::from_promise(*p)) { }
    std::coroutine_handle<promise_type> handle;
};

// 斐波那契数列生成器
Generator<int> fibonacci(int limit) {
    int a = 0, b = 1;
    for (int i = 0; i < limit; ++i) {
        co_yield a;
        int temp = a;
        a = b;
        b = temp + b;
    }
}

int main() {
    // 惰性生成:只在需要时计算下一个值
    for (int value : fibonacci(10)) {
        std::cout << value << " ";  // 0 1 1 2 3 5 8 13 21 34
    }
}



4.3 异步操作的顺序编排


协程用同步风格编写异步代码,避免“回调地狱”。


cpp复制下载

// 协程:像同步代码一样编排异步操作
awaitable<void> processUserData(const std::string& userId) {
    // 顺序执行多个异步操作,代码风格像同步
    auto userInfo = co_await fetchUserInfo(userId);      // 异步查询用户
    auto orders = co_await fetchUserOrders(userId);      // 异步查询订单
    auto recommendations = co_await fetchRecommendations(userInfo); // 异步推荐
    
    // 处理结果
    renderPage(userInfo, orders, recommendations);
}



五、选择决策流程


图表代码下载全屏

六、混合使用:线程 + 协程


在实际项目中,线程和协程经常混合使用,发挥各自优势:


cpp复制下载

// 多线程 + 每线程内协程:充分利用多核 + 高并发 I/O
class HybridServer {
    std::vector<std::thread> workerThreads_;
    
    void workerLoop(boost::asio::io_context& ioCtx) {
        ioCtx.run();  // 事件循环驱动协程
    }
    
public:
    void start(int numThreads) {
        boost::asio::io_context ioCtx;
        
        // 在事件循环上启动协程
        co_spawn(ioCtx, listener(ioCtx), detached);
        
        // 启动多个工作线程(通常等于 CPU 核心数)
        for (int i = 0; i < numThreads; ++i) {
            workerThreads_.emplace_back(&HybridServer::workerLoop, 
                                        this, std::ref(ioCtx));
        }
    }
};
// 这种设计在 Nginx、Redis、Seastar 等高性能系统中广泛应用



七、总结


线程和协程的选择可以归纳为以下核心原则:



  1. 线程用于 CPU 密集和真正并行:需要利用多核 CPU 的计算能力时必须使用线程。线程由操作系统抢占式调度,适合计算密集型任务、阻塞式系统调用和需要与现有同步模型集成的场景。缺点是内存开销大(每线程独立栈)、切换代价高。
  2. 协程用于高并发 I/O 和异步编排:协程在用户态通过事件循环协作式调度,切换代价极低,内存占用小。适合高并发网络服务、生成器/惰性求值和异步操作的顺序编排。缺点是不提供真正并行,协程内的阻塞操作会阻塞整个线程。
  3. 混合使用是现代高性能系统的最佳实践:使用少量线程(通常等于 CPU 核数)充分利用多核,每个线程内运行大量协程处理海量并发连接。这种架构兼具多核并行和高并发 I/O 的优势。
  4. 选择的关键因素



  • 任务性质:CPU 密集型 → 线程;I/O 密集型 → 协程
  • 并发规模:低并发 → 线程池;高并发(万级+)→ 协程
  • 编程复杂度:同步阻塞代码 → 线程;异步非阻塞 → 协程
  • 资源限制:内存紧张 → 协程;内存充裕 → 线程也可接受


线程和协程并非互斥,而是互补。在高性能 C++ 应用中,合理的架构往往是“少量线程 + 大量协程”的组合,各取所长。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐