1. 项目概述:为什么C++11的多线程是“语言层面”的革命
如果你在C++11之前写过跨平台的多线程程序,那你一定对那段“黑暗岁月”记忆犹新。那时候,你要么依赖操作系统提供的原生API,比如Windows的CreateThread和Linux的pthread_create,代码里充斥着大量的#ifdef _WIN32;要么就得引入像Boost.Thread这样的第三方库。这两种方式都让代码变得臃肿、难以移植,而且极易出错——内存泄漏、数据竞争、死锁,这些“坑”一个接一个。
C++11标准引入的多线程支持,彻底改变了这个局面。它不是在标准库外面包了一层,而是从语言核心和标准库层面,为并发编程提供了一整套原生的、类型安全的、可移植的抽象。这意味着,多线程编程从“系统调用”或“库函数”的范畴,正式晋升为“C++语言特性”。我们不再需要关心底层是POSIX线程还是Windows线程,编译器会为我们处理好一切。std::thread、std::mutex、std::future这些家伙,现在和std::vector、std::string一样,是C++标准家庭的一员。
这套机制解决了几个核心痛点:可移植性(一份代码,多处编译)、类型安全(告别void*和强制转换)、资源管理(利用RAII自动管理线程和锁的生命周期)。对于从Java、C#等语言转过来的开发者,可能会觉得“这不就是标配吗?”,但对于老C++程序员来说,这无疑是雪中送炭。它让编写正确、高效、清晰的多线程代码,门槛大大降低。接下来,我们就从最基础的线程创建与管理开始,完整拆解这套“语言武器库”。
2. 核心基石:线程的创建、管理与生命周期
2.1std::thread:线程对象化与资源管理
在C++11中,线程被抽象为一个对象,即std::thread。这是最根本的哲学转变:线程是一种资源,其生命周期应该由对象来管理(RAII原则)。创建一个线程非常简单:
#include <iostream> #include <thread> void hello() { std::cout << "Hello from thread! Thread ID: " << std::this_thread::get_id() << std::endl; } int main() { // 创建一个线程,执行hello函数 std::thread t(hello); // 主线程继续执行自己的工作 std::cout << "Hello from main! Main Thread ID: " << std::this_thread::get_id() << std::endl; // 等待子线程t执行完毕 t.join(); return 0; }这里有几个关键点:
- 构造即启动:
std::thread t(hello);这行代码不仅构造了线程对象t,还立即启动了一个新的执行线程去运行hello函数。这与某些语言(如Java的Thread)需要显式调用start()方法不同。 - 可调用对象:构造函数的参数可以是任何可调用对象——普通函数、函数对象(仿函数)、Lambda表达式、成员函数指针(需要绑定对象)等。这提供了极大的灵活性。
- 分离与汇合:每个
std::thread对象都对应一个底层系统线程。你必须在线程对象销毁前,明确它的“身后事”:join():阻塞当前线程(通常是主线程),直到被join的线程执行完毕。这确保了子线程的资源被正确清理。join之后,std::thread对象不再关联任何线程(joinable() == false)。detach():将线程对象与底层执行线程分离。分离后的线程变为“守护线程”,独立运行,其资源在线程结束时由系统自动回收。分离后,你无法再通过该对象与之交互。这是非常危险的操作,除非你非常清楚该线程的生命周期和资源管理,否则不建议使用。
注意:如果一个
std::thread对象在析构时仍然是joinable的(即既没有join也没有detach),程序会调用std::terminate()直接终止!这是C++11多线程编程的第一个,也是最重要的“坑”。务必在std::thread对象离开作用域前,决定好是join还是detach。
2.2 线程标识、硬件并发数与让出CPU
除了std::thread类本身,<thread>头文件还提供了几个在命名空间std::this_thread下的实用函数:
std::this_thread::get_id():获取当前线程的唯一标识符(std::thread::id类型)。这个ID主要用于调试和日志,或者作为容器的键(例如,线程局部存储)。std::thread::hardware_concurrency():一个静态函数,返回当前系统支持的真正并发执行的线程数(通常是CPU核心数或超线程数)。这个值对于决定线程池大小等任务划分策略至关重要,它是一个提示值,可能返回0(如果无法获取)。std::this_thread::yield():提示调度器,当前线程愿意放弃剩余的CPU时间片,让其他线程运行。这在自旋锁或忙等待循环中非常有用,可以避免无谓的CPU空转。但要注意,它只是一个“提示”,操作系统调度器可能忽略它。
// 示例:使用硬件并发数 unsigned int n = std::thread::hardware_concurrency(); std::cout << "This machine supports about " << n << " concurrent threads.\n"; // 示例:在忙等待循环中使用yield while (!data_is_ready.load()) { std::this_thread::yield(); // 让出CPU,避免死循环占满核心 }3. 同步原语:保护共享数据与协调执行顺序
多个线程访问共享数据,如果不加控制,就会导致数据竞争,结果是未定义的(通常是程序崩溃或数据错误)。C++11提供了一系列同步原语来构建“临界区”,确保同一时间只有一个线程能访问共享资源。
3.1std::mutex:互斥锁的基础与变体
最基本的锁是std::mutex。它提供了lock(),try_lock(),unlock()三个基本操作。但直接使用这些原始接口非常容易出错(比如忘记unlock导致死锁)。因此,永远优先使用RAII包装器。
#include <mutex> std::mutex g_mutex; int shared_data = 0; void unsafe_increment() { g_mutex.lock(); ++shared_data; // 临界区 // 如果这里抛出异常,mutex将永远不会被解锁! g_mutex.unlock(); } void safe_increment() { std::lock_guard<std::mutex> lock(g_mutex); // 构造时加锁 ++shared_data; // 临界区 // lock_guard析构时自动解锁,即使发生异常 }std::lock_guard是最简单的RAII锁管理器,构造时加锁,析构时解锁。但它不够灵活(例如,无法中途解锁)。这时可以使用std::unique_lock,它提供了更丰富的功能:延迟加锁(defer_lock)、尝试加锁(try_lock)、手动加解锁、所有权转移等。
std::mutex mtx; std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 构造但不加锁 // ... 做一些不需要锁的准备工作 ... lock.lock(); // 手动加锁 // ... 临界区 ... lock.unlock(); // 可以手动提前解锁 // ... 做一些非临界区工作 ... // 离开作用域时,如果锁仍被持有,会自动解锁除了标准互斥锁,C++11还提供了几种变体:
std::recursive_mutex:允许同一个线程多次加锁,防止自身死锁。但滥用它通常意味着设计有问题(比如在公有函数内部调用了另一个需要同一把锁的公有函数)。std::timed_mutex/std::recursive_timed_mutex:除了基本操作,还提供了try_lock_for()和try_lock_until(),允许尝试加锁一段时间,超时则失败。这在避免长时间死等时有用。std::shared_mutex(C++17):读写锁。允许多个线程并发读,但写是独占的。对于读多写少的场景能大幅提升性能。
3.2 死锁预防与std::lock和std::scoped_lock
死锁的经典场景是“哲学家就餐问题”,在代码中常表现为两个线程互相等待对方持有的锁。C++11提供了工具来一次性锁定多个互斥量,从而避免因加锁顺序不一致导致的死锁。
std::mutex mtx1, mtx2; // 错误示例:可能死锁 void thread_a_bad() { std::lock_guard<std::mutex> lock1(mtx1); std::this_thread::sleep_for(std::chrono::milliseconds(1)); // 增加死锁概率 std::lock_guard<std::mutex> lock2(mtx2); // 可能在此等待,而thread_b正持有mtx2等待mtx1 // ... } // 正确示例:使用std::lock void thread_a_good() { std::unique_lock<std::mutex> lock1(mtx1, std::defer_lock); std::unique_lock<std::mutex> lock2(mtx2, std::defer_lock); std::lock(lock1, lock2); // 一次性锁定两个锁,内部使用死锁避免算法 // 临界区 // lock1, lock2析构时自动解锁 }C++17引入了std::scoped_lock,它是std::lock_guard的增强版,能接受多个互斥量,并在构造时一次性锁定它们,语法更简洁。
// C++17 最佳实践 void thread_a_best() { std::scoped_lock lock(mtx1, mtx2); // 构造时一次性锁定所有mutex // 临界区 // 析构时按相反顺序解锁所有mutex }3.3 条件变量:线程间的通知与等待
互斥锁解决了互斥访问的问题,但线程间经常需要协作:一个线程等待某个条件成立(如队列非空),而另一个线程在条件成立时通知它。这就是std::condition_variable的用武之地。它必须与一个std::mutex(通常通过std::unique_lock)配合使用。
#include <iostream> #include <thread> #include <mutex> #include <condition_variable> #include <queue> std::mutex mtx; std::condition_variable cv; std::queue<int> data_queue; bool finished = false; // 生产者线程 void producer() { for (int i = 0; i < 10; ++i) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); { std::lock_guard<std::mutex> lock(mtx); data_queue.push(i); std::cout << "Produced: " << i << std::endl; } cv.notify_one(); // 通知一个等待的消费者 } { std::lock_guard<std::mutex> lock(mtx); finished = true; } cv.notify_all(); // 通知所有消费者结束 } // 消费者线程 void consumer(int id) { while (true) { std::unique_lock<std::mutex> lock(mtx); // 等待条件:队列非空或生产结束 cv.wait(lock, []{ return !data_queue.empty() || finished; }); // 被唤醒后,需要重新检查条件(防止虚假唤醒) if (!data_queue.empty()) { int data = data_queue.front(); data_queue.pop(); lock.unlock(); // 可以提前解锁,减少锁的持有时间 std::cout << "Consumer " << id << " consumed: " << data << std::endl; } else if (finished) { // 队列空且生产结束,退出循环 break; } } std::cout << "Consumer " << id << " exited.\n"; } int main() { std::thread prod(producer); std::thread cons1(consumer, 1); std::thread cons2(consumer, 2); prod.join(); cons1.join(); cons2.join(); return 0; }关键点解析:
cv.wait(lock, predicate):这是条件变量的核心。它会原子地执行lock.unlock()并阻塞当前线程,直到被其他线程的notify_one()或notify_all()唤醒。被唤醒后,它会重新获取锁(lock.lock()),然后检查predicate(一个返回bool的lambda或函数)。如果predicate为true,则wait返回,继续执行;如果为false,则再次释放锁并进入等待。这个“检查-等待”循环完美解决了“虚假唤醒”问题。- 虚假唤醒:即使没有线程调用
notify,等待的线程也可能被操作系统唤醒。因此,永远不要使用单参数的wait(lock),而应该使用带谓词的双参数版本,或者在一个while循环中检查条件。 notify_one()与notify_all():前者只唤醒一个等待线程(具体哪个不确定),后者唤醒所有等待线程。根据你的场景选择,通常一对一时用notify_one,一对多或多对多时用notify_all。
实操心得:条件变量的使用模式非常固定:在修改条件(如向队列放入数据)后
notify;在等待条件时,使用wait配合谓词。务必在持有锁的情况下修改“条件”相关的共享变量(如finished和data_queue),并在相同的锁保护下进行wait。
4. 异步操作与未来结果:std::async与std::future
有时我们并不想手动管理线程,只是希望异步执行一个任务,并在未来的某个时刻获取其结果。C++11的<future>头文件提供了这个高级抽象。
4.1std::async:最简单的异步任务启动器
std::async是一个函数模板,它尝试启动一个异步任务(可能在新线程中,也可能在调用get()时同步执行,取决于启动策略),并返回一个std::future对象,用于获取最终结果。
#include <iostream> #include <future> #include <chrono> int compute_heavy_task(int x) { std::this_thread::sleep_for(std::chrono::seconds(2)); return x * x; } int main() { // 启动异步任务 std::future<int> fut = std::async(std::launch::async, compute_heavy_task, 10); std::cout << "Main thread can do other work here...\n"; std::this_thread::sleep_for(std::chrono::seconds(1)); // 获取结果(如果任务未完成,会阻塞等待) int result = fut.get(); std::cout << "Result is: " << result << std::endl; // 输出 100 return 0; }启动策略:
std::launch::async:强制在新线程中异步执行任务。std::launch::deferred:延迟执行。任务不会立即启动,只在调用future的get()或wait()时,在调用者线程中同步执行。std::launch::async | std::launch::deferred(默认):由实现决定。编译器可能会根据系统负载等因素选择策略。因此,如果你明确需要并发,务必指定std::launch::async。
4.2std::future与std::promise:生产者-消费者模型的未来值
std::future是一个单向通信通道的“消费者”端,用于获取一个尚未准备好的值。而std::promise则是“生产者”端,用于设置这个值(或异常)。它们通常配对使用,跨越线程传递结果。
#include <future> #include <thread> #include <iostream> #include <stdexcept> void producer(std::promise<int> prom) { try { std::this_thread::sleep_for(std::chrono::seconds(1)); int result = 42; // 模拟计算结果 prom.set_value(result); // 生产者设置值 } catch (...) { // 如果计算中发生异常,传递给消费者 prom.set_exception(std::current_exception()); } } int main() { std::promise<int> prom; std::future<int> fut = prom.get_future(); // 从promise获取关联的future std::thread t(producer, std::move(prom)); // promise不可复制,需要移动 // 消费者在主线程等待结果 try { int result = fut.get(); // 阻塞直到生产者set_value std::cout << "Received result: " << result << std::endl; } catch (const std::exception& e) { std::cout << "Caught exception from producer: " << e.what() << std::endl; } t.join(); return 0; }关键特性:
future::get():只能调用一次。调用后,future的状态变为无效(valid() == false)。再次调用get()会导致std::future_error异常。future::wait():仅等待任务完成,不获取结果。future::wait_for()/wait_until():超时等待。promise和future通常用于一次性数据传递。对于多次通知,可以考虑std::condition_variable或std::atomic标志。
4.3std::shared_future
std::future是独占的(移动语义)。如果多个线程需要等待同一个异步结果,就需要std::shared_future。它可以通过future.share()从一个std::future转换而来,并且可以被复制,多个对象可以引用同一个共享状态。
std::future<int> fut = std::async(std::launch::async, [](){ return 7; }); std::shared_future<int> shared_fut = fut.share(); // fut现在无效了 // 现在可以传递shared_fut的副本给多个线程 auto func = [](std::shared_future<int> sf) { std::cout << "Result in thread: " << sf.get() << std::endl; }; std::thread t1(func, shared_fut); std::thread t2(func, shared_fut); t1.join(); t2.join();5. 原子操作与内存模型:无锁编程的基石
对于简单的计数器、标志位,使用互斥锁可能杀鸡用牛刀,开销太大。C++11提供了std::atomic模板,用于定义原子类型。对原子类型的操作是不可分割的,从而无需锁即可实现线程安全。
5.1std::atomic基本类型与操作
#include <atomic> #include <thread> #include <iostream> std::atomic<int> counter{0}; // 原子整型 void increment() { for (int i = 0; i < 100000; ++i) { counter.fetch_add(1, std::memory_order_relaxed); // 原子加1 // 等价于 ++counter; (但++操作符重载也是原子的) } } int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout << "Final counter value: " << counter.load() << std::endl; // 一定是200000 return 0; }std::atomic为整数和指针类型提供了丰富的原子操作:load(),store(),exchange(),compare_exchange_strong/weak(CAS操作),fetch_add,fetch_sub,fetch_and,fetch_or,fetch_xor等。
5.2 内存序:理解并发世界的“墙”
这是C++11多线程中最复杂、也最精髓的部分。std::atomic的每个操作都可以指定一个内存序参数,它定义了当前原子操作周围的其他内存访问(非原子变量)的可见性顺序。默认是std::memory_order_seq_cst(顺序一致性),最安全但性能开销最大。
内存序主要分为几类:
- 顺序一致性 (
memory_order_seq_cst):最强约束。所有线程看到的原子操作顺序都是一致的,且所有非原子变量的修改也会在原子操作处建立同步关系。像是一个全局的“栅栏”。这是默认选项,易于理解,但可能限制编译器和硬器的优化。 - 获取-释放语义 (
memory_order_acquire,memory_order_release,memory_order_acq_rel):- Release(释放):对某个原子变量A的
store(release)操作,保证在它之前的所有内存写操作(包括非原子变量),对于后续对同一个原子变量A执行load(acquire)的线程是可见的。 - Acquire(获取):对某个原子变量A的
load(acquire)操作,保证在它之后的所有内存读操作,都能看到之前那个对A的store(release)操作及其之前的所有写操作。 - 这构成了一个“同步对”(Synchronizes-with),常用于实现锁、屏障或生产者-消费者模式中的状态发布。
- Release(释放):对某个原子变量A的
// 使用获取-释放语义实现一个简单的自旋锁 class spinlock { std::atomic_flag flag = ATOMIC_FLAG_INIT; public: void lock() { while (flag.test_and_set(std::memory_order_acquire)) { // 获取 // 自旋等待 } } void unlock() { flag.clear(std::memory_order_release); // 释放 } };- 松散顺序 (
memory_order_relaxed):只保证原子操作本身的原子性,不提供任何线程间同步保证。其他内存操作的顺序可以任意重排。只适用于不需要同步,只需要原子计数的场景(比如统计次数)。
选择建议:除非你在进行极致的底层性能优化,并且完全理解其后果,否则始终使用默认的memory_order_seq_cst。获取-释放语义在实现无锁数据结构时非常关键。松散顺序使用场景极少,且极易出错。
5.3std::atomic_flag:最简单的原子布尔标志
std::atomic_flag是一个最简单的原子布尔类型,只有两个操作:test_and_set()和clear()。它保证是无锁的。常用于实现自旋锁(如上例)或作为其他更复杂原子操作的基础。
6. 线程局部存储:让数据属于线程
全局变量或静态变量是所有线程共享的。有时我们需要每个线程拥有该变量的独立副本,这就是线程局部存储。C++11引入了thread_local关键字。
#include <iostream> #include <thread> #include <sstream> thread_local int thread_specific_value = 0; // 每个线程有一份独立的副本 void print_id_and_value() { std::ostringstream oss; oss << "Thread ID: " << std::this_thread::get_id() << ", value: " << thread_specific_value << ", address: " << &thread_specific_value << std::endl; std::cout << oss.str(); ++thread_specific_value; // 修改只影响本线程的副本 } int main() { thread_specific_value = 100; // 设置主线程的副本 std::thread t1(print_id_and_value); // t1的副本初始化为0(静态初始化) std::thread t2(print_id_and_value); // t2的副本初始化为0 t1.join(); t2.join(); print_id_and_value(); // 主线程的副本现在是101 return 0; }thread_local可以用于全局变量、函数内的静态变量、类的静态成员变量。它的初始化是线程安全的(类似于函数内静态局部变量的初始化)。线程局部存储非常适合用于存储线程ID、随机数生成器、数据库连接等需要隔离的资源。
注意事项:
thread_local变量的析构顺序在C++中是有规定的(与构造顺序相反),但如果线程是调用std::quick_exit退出的,这些析构函数可能不会被调用。此外,过度使用thread_local可能会增加线程创建和销毁的开销。
7. 常见问题、陷阱与调试技巧实录
即使掌握了所有工具,多线程编程依然充满陷阱。这里记录一些我踩过的坑和总结的经验。
7.1 数据竞争与未定义行为
这是最隐蔽的错误。两个线程同时读写一个非原子、非受锁保护的变量,就是数据竞争。C++标准规定,数据竞争导致未定义行为。这意味着程序可能崩溃、产生错误结果,或者看似正常地运行(最可怕的情况)。
排查技巧:
- 使用工具:在开发阶段,务必使用线程检查工具。Linux下可以用
ThreadSanitizer(-fsanitize=thread),Windows下Visual Studio有强大的并发分析工具。 - 最小化共享数据:设计时,优先考虑无共享架构(如任务并行)。必须共享时,将其范围缩到最小。
- 一切共享皆需保护:对任何可能被多个线程访问的非常量、非原子数据,问自己:它是否被妥善保护了?
7.2 死锁的成因与避免
死锁的四个必要条件:互斥、持有并等待、不可剥夺、循环等待。打破任意一个即可避免。
实战建议:
- 固定锁的顺序:如果多个锁必须被同时持有,在所有线程中都以相同的顺序获取它们。这是最有效的方法。
- 使用
std::lock或std::scoped_lock:如前所述,它们内部使用死锁避免算法。 - 避免在持有锁时调用未知代码:特别是用户回调或虚函数,因为你不知道它内部会不会再去获取别的锁。
- 使用锁的层次结构:给锁定义层级,只允许持有高层锁的线程去获取低层锁,反之则报错或等待。
7.3 条件变量的使用误区
- 虚假唤醒:前面已强调,必须使用带谓词的
wait或在循环中检查条件。 - 丢失唤醒:如果消费者在生产者调用
notify之后才调用wait,那么这次通知就丢失了,消费者可能会永久等待。因此,条件变量的使用模式要求:在改变条件并通知时,必须持有与等待方相同的锁。这确保了状态的改变和通知是原子的,不会丢失。 notify_onevsnotify_all:错误选择可能导致部分线程饥饿。仔细分析你的场景:是单一事件通知一个任意线程,还是一个事件需要通知所有等待者?
7.4std::async的陷阱
- 默认启动策略的惰性求值:如果不指定
std::launch::async,任务可能被延迟到get()时在当前线程执行,这完全失去了并发意义。 future析构的阻塞:std::async返回的future的析构函数会阻塞,直到异步操作完成。这意味着如果你不保存这个future,它会在表达式结束时析构,导致隐式等待,可能让你误以为程序是并发的,实际上是串行的。
// 错误示例:看似并发,实则串行 void foo() { std::async(std::launch::async, []{ do_some_work(); }); // 返回的临时future立即析构,阻塞等待! // 直到do_some_work完成,才会执行下一行 do_other_work(); } // 正确做法:保存future void foo_correct() { auto fut = std::async(std::launch::async, []{ do_some_work(); }); // 保存future do_other_work(); // 真正并发执行 fut.get(); // 最后再等待结果(如果需要) }7.5 性能考量:锁粒度与无锁数据结构
- 锁的粒度要细:只锁住必须保护的数据和最短的代码路径。避免在持有锁时进行I/O操作、长时间计算或调用可能阻塞的函数。
- 考虑无锁编程:对于高性能热点路径,可以考虑无锁数据结构(如
boost::lockfree队列)。但无锁编程极其复杂,容易出错,除非性能瓶颈确凿且锁成为瓶颈,否则不要轻易尝试。std::atomic和内存序是构建无锁数据结构的基础。 - 测量,而不是猜测:多线程程序的性能行为往往反直觉。务必使用性能分析工具(如perf, VTune)来定位真正的热点,而不是盲目优化。
8. 实战:构建一个简单的线程安全队列
综合运用以上知识,我们实现一个比前面示例更健壮、更通用的线程安全队列模板。它使用互斥锁和条件变量,支持多生产者、多消费者。
#include <queue> #include <mutex> #include <condition_variable> #include <optional> template<typename T> class ThreadSafeQueue { private: mutable std::mutex mtx_; std::queue<T> queue_; std::condition_variable cv_; bool shutdown_ = false; // 优雅关闭标志 public: ThreadSafeQueue() = default; ~ThreadSafeQueue() { shutdown(); } // 禁止拷贝 ThreadSafeQueue(const ThreadSafeQueue&) = delete; ThreadSafeQueue& operator=(const ThreadSafeQueue&) = delete; void push(T value) { { std::lock_guard<std::mutex> lock(mtx_); if (shutdown_) return; // 已关闭,不再接受新数据 queue_.push(std::move(value)); } cv_.notify_one(); // 通知一个等待的消费者 } // 阻塞弹出,直到有数据或队列关闭 std::optional<T> pop() { std::unique_lock<std::mutex> lock(mtx_); cv_.wait(lock, [this]{ return !queue_.empty() || shutdown_; }); if (!queue_.empty()) { T value = std::move(queue_.front()); queue_.pop(); return value; } // 队列空且已关闭,返回空值 return std::nullopt; } // 非阻塞尝试弹出 std::optional<T> try_pop() { std::lock_guard<std::mutex> lock(mtx_); if (queue_.empty()) { return std::nullopt; } T value = std::move(queue_.front()); queue_.pop(); return value; } bool empty() const { std::lock_guard<std::mutex> lock(mtx_); return queue_.empty(); } size_t size() const { std::lock_guard<std::mutex> lock(mtx_); return queue_.size(); } // 优雅关闭:唤醒所有等待的消费者,使其退出 void shutdown() { { std::lock_guard<std::mutex> lock(mtx_); shutdown_ = true; } cv_.notify_all(); } };这个实现的关键改进点:
- 优雅关闭:通过
shutdown_标志和shutdown()方法,可以安全地停止队列,避免消费者线程永远阻塞在pop()上。 - 使用
std::optional:pop()返回std::optional<T>,可以清晰地区分“取到值”和“队列已关闭且为空”两种情况,比返回bool并通过输出参数获取值更现代、安全。 - 移动语义:
push和pop使用std::move,避免不必要的拷贝,提高性能。 - 异常安全:利用
std::lock_guard和std::unique_lock的RAII特性,确保发生异常时锁能被正确释放。 - const成员函数:
empty()和size()标记为const,但内部需要加锁,所以互斥量mtx_被声明为mutable。
这个队列可以作为线程池、生产者-消费者模型等并发模式的基础组件。在实际项目中,你可能还需要考虑设置队列容量上限(有界队列)、支持超时pop、更高效的内存分配(如使用节点池)等高级特性。
C++11的多线程库将并发编程从平台相关的泥潭中解放出来,提供了一套统一、类型安全、高效的抽象。从基础的std::thread和std::mutex,到高级的std::async和原子操作,再到底层的内存模型,它构建了一个层次分明的并发工具箱。掌握它,意味着你能够以现代C++的方式,编写出健壮、高效且可移植的多线程程序。虽然并发编程的复杂性不会消失,但至少,我们有了更趁手的武器去应对它。记住核心原则:用RAII管理资源,用锁或原子操作保护共享数据,用条件变量进行线程间通信,并始终对数据竞争和死锁保持警惕。在性能优化时,先测量,再动手,谨慎对待无锁编程。