C++多线程编程实战:从std::thread到异步并发
1. C++多线程编程实战:从std::thread到异步并发
在现代 C++ 开发中,多线程编程已经成为提升程序性能、充分利用多核处理器能力的关键技术。从 C++11 标准开始,标准库正式引入了线程支持库,其中 std::thread 是最基础的线程抽象;而随着 C++20 引入协程、以及标准库中 std::async、std::future 等组件的完善,C++ 的并发编程模型也日趋丰富。本文将从 std::thread 入手,逐步深入到异步并发编程,帮助读者建立一套完整、可落地的多线程实战知识体系。
2. 线程基础:std::thread 的使用
std::thread 是 C++ 标准库提供的线程类,它封装了操作系统原生线程,并提供了跨平台的统一接口。使用 std::thread 创建线程非常简单,只需要传入一个可调用对象即可。
#include <iostream>
#include <thread>
void hello() {
std::cout << "Hello from thread, id = " << std::this_thread::get_id() << std::endl;
}
int main() {
std::thread t(hello);
t.join(); // 等待线程结束
return 0;
}
在上面的示例中,std::thread t(hello) 创建了一个执行 hello 函数的新线程,随后通过 t.join() 阻塞主线程,直到子线程执行完毕。除了 join(),还可以调用 detach() 让线程在后台独立运行,但需要注意:一旦 detach() 之后,线程对象将不再与该线程关联,也无法再通过该对象等待线程结束。
3. 线程传参与返回值
向线程函数传递参数时,所有参数都会按值复制到线程的存储空间中。如果需要传递引用,必须显式使用 std::ref 包装,否则即使函数签名是引用类型,实际传入的也是副本。
#include <iostream>
#include <thread>
void add(int a, int b, int& result) {
result = a + b;
}
int main() {
int sum = 0;
std::thread t(add, 3, 4, std::ref(sum));
t.join();
std::cout << "sum = " << sum << std::endl; // 输出 sum = 7
return 0;
}
需要注意的是,std::thread 本身并不直接支持返回值。如果希望从线程中获取计算结果,通常的做法是通过引用参数、共享变量加锁,或者使用后面将要介绍的 std::async 与 std::future 机制。
4. 线程同步与互斥
当多个线程同时访问共享数据时,就会产生数据竞争(data race),进而导致未定义行为。C++ 标准库提供了 std::mutex 和 std::lock_guard 等工具来保护共享资源。
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
std::mutex mtx;
int counter = 0;
void increment(int times) {
for (int i = 0; i < times; ++i) {
std::lock_guard<std::mutex> lock(mtx);
++counter;
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 4; ++i) {
threads.emplace_back(increment, 10000);
}
for (auto& t : threads) {
t.join();
}
std::cout << "counter = " << counter << std::endl; // 输出 counter = 40000
return 0;
}
std::lock_guard 采用 RAII 机制,在构造时自动加锁,在析构时自动解锁,即使发生异常也能保证锁被正确释放,从而避免死锁和资源泄漏。对于需要更精细控制的场景,可以使用 std::unique_lock,它支持手动加锁、解锁以及延迟加锁等操作。
5. 条件变量与线程通信
条件变量(std::condition_variable)用于在线程之间传递事件通知,它通常与互斥锁配合使用,实现「等待-通知」的同步模式。典型的生产者-消费者模型就是条件变量的经典应用场景。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> data_queue;
void producer() {
for (int i = 0; i < 5; ++i) {
{
std::lock_guard<std::mutex> lock(mtx);
data_queue.push(i);
std::cout << "produced: " << i << std::endl;
}
cv.notify_one();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return !data_queue.empty(); });
int value = data_queue.front();
data_queue.pop();
lock.unlock();
std::cout << "consumed: " << value << std::endl;
if (value == 4) break;
}
}
int main() {
std::thread p(producer);
std::thread c(consumer);
p.join();
c.join();
return 0;
}
在消费者线程中,cv.wait(lock, predicate) 会先释放锁,然后进入阻塞状态;当生产者调用 notify_one() 后,消费者被唤醒并重新获取锁,再检查谓词条件是否满足。使用带谓词的 wait 可以避免虚假唤醒(spurious wakeup)带来的问题。
6. 异步并发:std::async 与 std::future
与手动管理 std::thread 不同,std::async 提供了一种更高层的异步任务抽象。它会在合适的时机启动一个异步任务,并返回一个 std::future 对象,通过该对象可以获取任务的执行结果。
#include <iostream>
#include <future>
int compute(int a, int b) {
return a * b;
}
int main() {
std::future<int> result = std::async(std::launch::async, compute, 6, 7);
std::cout << "result = " << result.get() << std::endl; // 输出 result = 42
return 0;
}
std::async 的第一个参数是启动策略:std::launch::async 表示强制在新线程中执行;std::launch::deferred 表示延迟到调用 get() 或 wait() 时才在调用线程中执行;默认策略则允许实现自行选择。调用 future.get() 会阻塞当前线程,直到异步任务完成并返回结果。
7. std::packaged_task 与 std::promise
除了 std::async,C++ 还提供了 std::packaged_task 和 std::promise 两种更灵活的异步工具。std::packaged_task 将一个可调用对象包装起来,并把其返回值与 std::future 关联;std::promise 则允许在线程之间手动传递值或异常。
#include <iostream>
#include <future>
#include <thread>
void set_value(std::promise<int> prom) {
prom.set_value(100);
}
int main() {
std::promise<int> prom;
std::future<int> fut = prom.get_future();
std::thread t(set_value, std::move(prom));
std::cout << "value = " << fut.get() << std::endl; // 输出 value = 100
t.join();
return 0;
}
需要注意的是,std::promise 和 std::packaged_task 都是不可复制、只能移动的对象,因此在传递给线程时需要使用 std::move。这种机制非常适合在复杂的任务调度场景中,把某个线程的计算结果安全地传递给另一个线程。
8. 原子操作与无锁编程
对于简单的计数器、标志位等场景,使用互斥锁可能带来不必要的性能开销。此时可以使用 std::atomic 原子类型,它基于硬件提供的原子指令实现,无需加锁即可保证操作的原子性。
#include <iostream>
#include <thread>
#include <atomic>
#include <vector>
std::atomic<int> counter{0};
void increment(int times) {
for (int i = 0; i < times; ++i) {
counter.fetch_add(1, std::memory_order_relaxed);
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 4; ++i) {
threads.emplace_back(increment, 10000);
}
for (auto& t : threads) {
t.join();
}
std::cout << "counter = " << counter.load() << std::endl; // 输出 counter = 40000
return 0;
}
原子操作还支持内存序(memory order)参数,用于控制操作的可见性和重排序约束。对于大多数场景,使用默认的 std::memory_order_seq_cst 即可保证正确性;只有在追求极致性能且对底层内存模型有深入理解时,才建议使用 relaxed、acquire、release 等更宽松的内存序。
9. 线程池与任务队列
频繁创建和销毁线程的开销较大,因此在处理大量短小任务时,通常会使用线程池来复用线程。下面给出一个简单的线程池实现,它基于任务队列和条件变量构建。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <functional>
#include <vector>
class ThreadPool {
public:
explicit ThreadPool(size_t threads) : stop(false) {
for (size_t i = 0; i < threads; ++i) {
workers.emplace_back([this] {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(queue_mutex);
cv.wait(lock, [this] { return stop || !tasks.empty(); });
if (stop && tasks.empty()) return;
task = std::move(tasks.front());
tasks.pop();
}
task();
}
});
}
}
template<class F>
void enqueue(F&& f) {
{
std::unique_lock<std::mutex> lock(queue_mutex);
tasks.emplace(std::forward<F>(f));
}
cv.notify_one();
}
~ThreadPool() {
{
std::unique_lock<std::mutex> lock(queue_mutex);
stop = true;
}
cv.notify_all();
for (auto& worker : workers) {
worker.join();
}
}
private:
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex queue_mutex;
std::condition_variable cv;
bool stop;
};
int main() {
ThreadPool pool(4);
for (int i = 0; i < 8; ++i) {
pool.enqueue([i] {
std::cout << "task " << i << " executed by thread "
<< std::this_thread::get_id() << std::endl;
});
}
return 0;
}
这个线程池在构造时创建固定数量的工作线程,并通过任务队列接收外部提交的任务。析构时设置停止标志并唤醒所有线程,等待它们处理完剩余任务后退出。在实际项目中,还可以在此基础上扩展支持带返回值的任务、任务优先级、动态扩容等能力。
10. 常见陷阱与最佳实践
多线程编程中容易踩坑的地方很多,这里总结几个最常见的陷阱和对应的最佳实践。
- 数据竞争:多个线程同时读写同一变量而未加同步,会导致未定义行为。解决方法是使用互斥锁、原子变量或线程局部存储。
- 死锁:多个线程以不同顺序获取多个锁时可能产生死锁。建议使用
std::lock一次性锁定多个互斥量,或保证所有线程按相同顺序加锁。 - 悬空引用:向线程传递局部变量的引用或指针,而该变量在线程执行前已被销毁。应确保线程访问的对象生命周期足够长,或按值传递。
- 忘记 join 或 detach:
std::thread析构时如果线程仍可 join,会调用std::terminate。务必在析构前明确调用join()或detach()。 - 过度加锁:锁粒度过大会降低并发性能。应尽量缩小临界区范围,只在必要时加锁。
本文从 std::thread 的基础用法出发,系统介绍了 C++ 多线程编程的核心内容,包括线程创建与传参、互斥锁与条件变量、异步任务(std::async、std::future、std::packaged_task、std::promise)、原子操作以及线程池的实现。掌握这些工具之后,读者可以根据实际场景选择合适的并发模型:简单任务优先考虑 std::async,需要精细控制线程生命周期时使用 std::thread,高并发短任务场景则适合引入线程池。多线程编程的难点不仅在于 API 的使用,更在于对数据竞争、死锁等并发问题的深刻理解,建议读者在实战中多写多练,逐步积累经验。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)