C++并发编程实战笔记:从内存模型到原子操作与线程同步
1. 项目概述为什么我们需要一本《C并发编程实战》的笔记如果你是一名C开发者无论是刚入行还是已经写了几年业务代码迟早有一天会碰到“并发”这个坎。我刚开始接触并发编程时感觉就像在玩一个没有说明书的复杂乐高套装零件线程很多说明书编程模型却语焉不详稍有不慎拼出来的就不是飞船而是一堆随时会散架的碎片。市面上关于C并发的书不少但Anthony Williams的《C并发编程实战》无疑是其中最经典、最深入的一本。它不像有些书只讲语法糖而是从内存模型、原子操作这些底层基石讲起帮你建立起对并发程序正确性的坚实认知。然而这本书的“实战”二字也意味着它的深度和广度。直接啃原书尤其是前几章关于内存序和原子操作的讨论很容易让人迷失在细节里忘了最初的目标。这就是我做这份笔记的初衷它不是原书的简单摘抄而是我结合自己多年在后台服务、游戏服务器、高频交易等场景下踩坑填坑的经验对书中核心概念进行的消化、重构和实战注解。我会用更直白的语言解释那些拗口的术语用具体的代码示例展示抽象概念如何落地并重点标注那些原书可能一笔带过、但在实际项目中能让你调试到崩溃的“魔鬼细节”。这份笔记一、二主要覆盖原书的前半部分精华即并发编程的基础设施和核心构建块。我们将从“为什么并发这么难”开始逐步拆解C11/14/17标准为我们提供的工具箱并始终围绕一个核心问题如何写出既正确安全无错又高效性能达标的并发代码。无论你是想面试通关还是让手头的项目性能翻倍这里的内容都将是你坚实的起点。2. 核心基石理解内存模型与原子操作很多人在学习并发时一上来就直奔std::thread和std::mutex这就像学开车只学踩油门和打方向盘却不了解交通规则和汽车原理上路必出事故。C并发编程的“交通规则”就是内存模型而“汽车原理”的核心部件之一就是原子操作。2.1 顺序一致性一个美好的幻觉我们写的代码在单线程环境下执行顺序就是源码的顺序这被称为“顺序一致性”。这是最符合人类直觉的模型。但在多核处理器时代编译器为了优化可能会重排指令CPU为了效率也会乱序执行甚至每个核心都有自己的缓存导致一个线程的写入不会立刻被其他线程看到。假设没有明确的规则约束两个线程读写共享数据就会陷入混沌。C标准定义的内存模型就是为了在多线程的混沌中建立秩序告诉开发者和编译器哪些操作必须保证什么样的可见性和顺序。std::atomic和相关内存序就是我们用来与这个模型对话的工具。2.2std::atomic不只是“不可分割”提到原子操作很多人的第一反应是“不会被线程调度打断的单一操作”。这没错但只对了一半。在C中std::atomic的关键作用在于它定义了数据访问的同步语义。一个原子变量的读写不仅仅是一个操作更是在多个线程间建立了一道“同步栅栏”。举个例子我们有一个简单的标志位std::atomicbool data_ready(false); int data_value 0; // 线程A生产数据 void producer() { data_value 42; // 1. 存储数据 data_ready.store(true); // 2. 设置就绪标志 } // 线程B消费数据 void consumer() { while (!data_ready.load()) { // 3. 等待标志 std::this_thread::yield(); } std::cout data_value std::endl; // 4. 使用数据 }这里data_ready是原子布尔量。线程A先写data_value再设置data_ready为true。线程B循环读取data_ready看到true后才去读data_value。你可能会想即使没有atomic大部分时候这样写好像也能工作但这就是并发中最危险的“好像”。在没有同步的情况下编译器或CPU完全可能将线程A的步骤1和2重排或者线程B的读取看不到线程A的最新写入由于缓存导致线程B读到data_ready为true但data_value还是旧值0。std::atomic默认使用memory_order_seq_cst顺序一致性内存序它保证了原子操作本身具有全局顺序并且会阻止其前后非原子操作的重排从而确保了上述代码的正确性。这就是原子操作的核心价值它不仅是原子性的更是同步性的。注意std::atomic对于整型、指针等类型提供了丰富的成员函数如fetch_add,exchange。但对于自定义类型需要满足可平凡复制Trivially Copyable才能使用std::atomic否则你需要使用std::mutex来保护。这是选择工具时首先要做的判断。2.3 内存序精细控制同步的阀门如果memory_order_seq_cst能保证最强的一致性为什么还需要其他内存序答案是性能。顺序一致性要求太高相当于在所有原子操作处设置了全局路障会严重限制编译器和硬件的优化空间。C提供了更宽松的内存序让我们可以在保证正确性的前提下榨取更多性能。memory_order_relaxed只保证原子操作本身的原子性不提供任何同步或顺序保证。它就像在一个嘈杂的房间里每个人都在喊话但你无法确定谁先谁后听到。通常用于计数器等场景例如std::atomicint counter; counter.fetch_add(1, std::memory_order_relaxed);我们只关心最终计数准确不关心哪个线程的加法先被看到。memory_order_acquire和memory_order_release这是一对“搭档”用于构建“同步-with”关系。release操作如store之前的所有内存写入包括非原子写入都对后续在另一个线程中对同一原子变量执行acquire操作如load之后的读操作可见。这创建了一种“生产者-消费者”的同步。上面的例子可以优化为// 线程A data_value 42; data_ready.store(true, std::memory_order_release); // release 操作 // 线程B while (!data_ready.load(std::memory_order_acquire)) { // acquire 操作 // yield } std::cout data_value std::endl; // 这里保证能看到42这比seq_cst更高效因为它只约束了相关线程间的顺序而非全局顺序。memory_order_acq_rel读-改-写操作如fetch_add,exchange使用同时具有acquire和release语义。常用于实现锁或复杂的同步原语。memory_order_consume一个更弱、更复杂且已被不鼓励使用的模型涉及数据依赖关系。在大多数代码中你应该避免使用它直接用acquire/release更安全。实操心得对于绝大多数应用开发者我的建议是默认使用memory_order_seq_cst。除非你正在编写极低延迟的核心库如锁、无锁数据结构并且已经通过性能剖析Profiling确定内存序是瓶颈否则不要轻易使用宽松内存序。错误的宽松内存序带来的bug极其隐蔽调试成本远高于那一点性能提升。先保证正确再考虑优化。3. 线程管理从创建到生命周期的完全掌控有了内存模型的理论基础我们来看看如何创建和管理并发的基本单元——线程。C11的std::thread将线程从平台相关API中抽象出来用法直观但细节决定成败。3.1 线程的创建与基本控制创建一个线程非常简单void hello() { std::cout Hello from thread!\\n; } int main() { std::thread t(hello); // 1. 创建并立即启动线程执行hello函数 t.join(); // 2. 等待线程t结束 return 0; }这里有两个关键点第一线程对象t在构造时新线程就开始执行了。第二在t析构前你必须决定它的命运join()等待它结束或detach()分离它让其后台运行。如果两者都没做std::thread的析构函数会调用std::terminate()终止整个程序这是一个常见的崩溃原因。向线程传递参数遵循标准函数调用规则但需要注意引用和指针的生命周期void update_data(int data) { data 2; } int main() { int local_data 1; // 错误local_data的引用可能在线程运行时已失效如果main先结束 // std::thread t(update_data, local_data); // 正确做法1传递指针但需确保指向的对象生命周期足够长 std::thread t(update_data, std::ref(local_data)); // 使用std::ref传递引用包装器 t.join(); std::cout local_data std::endl; // 输出2 // 正确做法2通过lambda捕获按值或按引用同样要注意生命周期 std::thread t2([local_data]() { local_data 3; }); t2.join(); std::cout local_data std::endl; // 输出3 return 0; }注意事项当线程函数通过引用或指针访问主线程或其它线程的局部变量时必须确保这些变量的生命周期覆盖线程的执行时间。最安全的方式是按值传递或者传递指向堆内存需妥善管理所有权如用std::shared_ptr或全局/静态数据的指针。3.2 线程标识、数量与让出std::this_thread::get_id()获取当前线程的唯一ID常用于日志调试。std::thread::hardware_concurrency()返回硬件支持的并发线程数通常是CPU核心数。这是设置线程池大小的一个重要参考但并非金科玉律需考虑I/O等待、超线程等因素。std::this_thread::yield()提示调度器让出当前线程的时间片。在忙等待busy-wait循环中适当使用yield可以降低CPU占用。例如在自旋锁spinlock或某些无锁算法的等待循环中while (!flag.load(std::memory_order_acquire)) { std::this_thread::yield(); // 让出CPU避免空转耗电 }3.3 线程所有权转移与std::jthread(C20)std::thread是**可移动movable但不可复制copyable**的。这意味着线程的所有权可以在std::thread对象间转移这为创建线程池或管理线程生命周期提供了灵活性。std::thread t1([]{ /* ... */ }); // std::thread t2 t1; // 错误不能复制 std::thread t2 std::move(t1); // 正确所有权转移t1不再代表任何线程 // 现在由t2负责join或detachC20引入了std::jthreadjoining thread它最大的改进是在析构时自动join()避免了忘记join导致的程序终止。此外它还内置了停止令牌std::stop_token支持用于更优雅地请求线程停止这是手动管理std::thread时比较繁琐的部分。如果你的项目能用C20优先考虑std::jthread。4. 共享数据保护互斥锁Mutex的深入剖析当多个线程需要读写同一份数据时互斥锁Mutex是最直接、最常用的同步工具。它的原理很简单给一段代码临界区加锁确保同一时间只有一个线程能执行它。4.1 锁的基本使用与RAII技法直接使用std::mutex的lock()和unlock()是容易出错的因为异常或提前返回可能导致锁无法释放造成死锁。std::mutex mtx; int shared_data 0; void unsafe_increment() { mtx.lock(); shared_data; // 如果这里抛出异常锁永远不会被释放 mtx.unlock(); }正确的做法是使用**RAII资源获取即初始化**包装器std::lock_guard和std::unique_lock。void safe_increment() { std::lock_guardstd::mutex lock(mtx); // 构造时加锁析构时自动解锁 shared_data; } // lock_guard析构自动调用mtx.unlock() void flexible_increment() { std::unique_lockstd::mutex lock(mtx); // 同样RAII但功能更多 shared_data; // 可以手动解锁在锁保护范围外执行一些不涉及共享数据的操作 lock.unlock(); // ... 执行其他耗时操作 ... // 如果需要再次加锁可以调用 lock.lock(); }std::lock_guard简单轻量适用于大多数简单的临界区场景。std::unique_lock更灵活支持延迟加锁、条件变量、所有权转移等但开销稍大。4.2 死锁成因与破解之道死锁通常发生在需要同时获取多个锁的时候。经典条件是互斥、持有并等待、不可剥夺、循环等待。// 线程A std::lock_guardstd::mutex lock_a(mtx_a); std::this_thread::sleep_for(1ms); // 模拟一些操作 std::lock_guardstd::mutex lock_b(mtx_b); // 尝试获取mtx_b // 线程B std::lock_guardstd::mutex lock_b(mtx_b); std::this_thread::sleep_for(1ms); std::lock_guardstd::mutex lock_a(mtx_a); // 尝试获取mtx_a // 结果A等B的mtx_bB等A的mtx_a死锁发生。解决方案1固定锁的顺序所有线程都按相同的全局顺序获取锁例如总是先锁mtx_a再锁mtx_b。这需要你在设计时规划好。解决方案2使用std::lock一次性锁定多个互斥量C标准库提供了std::lock函数它可以一次性锁定两个或更多个互斥量且保证不会死锁通常使用某种死锁避免算法如try-and-backoff。void safe_transaction(std::mutex mtx1, std::mutex mtx2) { // 一次性锁定两个锁避免中间状态导致死锁 std::lock(mtx1, mtx2); // 使用lock_guard接管锁的所有权adopt_lock表示已锁定 std::lock_guardstd::mutex lock1(mtx1, std::adopt_lock); std::lock_guardstd::mutex lock2(mtx2, std::adopt_lock); // ... 操作受保护的数据 ... }解决方案3使用std::scoped_lock(C17)这是C17引入的更方便的RAII包装器可以同时锁定多个互斥量是std::lock的RAII版本。std::mutex mtx_a, mtx_b; void safe_transaction_cpp17() { std::scoped_lock lock(mtx_a, mtx_b); // 一次性锁定所有析构时按相反顺序解锁 // ... 操作共享数据 ... }std::scoped_lock是现代C中处理多个锁的首选方式。4.3 其他类型的互斥量std::recursive_mutex允许同一个线程多次加锁。常用于递归函数或可能被同一线程多次调用的函数。但需谨慎使用因为它掩盖了设计上的问题为何需要重复加锁且性能通常低于普通mutex。std::timed_mutex和std::recursive_timed_mutex除了lock()还提供了try_lock_for()和try_lock_until()允许尝试加锁一段时间超时则失败。适用于避免长时间阻塞的场景。std::shared_mutex(C17)读写锁。允许多个线程同时读但写操作是独占的。适用于读多写少的场景。使用std::shared_lock进行共享读锁定std::unique_lock或std::lock_guard进行独占写锁定。实操心得锁的粒度要尽可能小。只锁住真正需要保护的数据和最短的代码路径。长时间持有锁会严重降低并发性能。另外尽量避免在锁内调用未知代码如虚函数、回调函数因为这可能间接导致死锁对方也在等你持有的锁或性能问题。5. 高级同步机制条件变量、期值与承诺互斥锁解决了互斥访问的问题但线程间协作常常需要更复杂的机制一个线程需要等待某个条件成立或者需要获取另一个线程的计算结果。5.1 条件变量等待与通知条件变量std::condition_variable允许一个或多个线程等待阻塞直到被另一个线程通知notify某个条件可能已满足。它总是与一个互斥量和一个条件通常是共享变量的状态一起使用。经典的生产者-消费者队列示例std::mutex mtx; std::queueint data_queue; std::condition_variable data_cond; void producer() { for (int i 0; i 10; i) { std::this_thread::sleep_for(100ms); { std::lock_guardstd::mutex lock(mtx); data_queue.push(i); std::cout Produced: i std::endl; } data_cond.notify_one(); // 通知一个等待的消费者 } } void consumer() { while (true) { std::unique_lockstd::mutex lock(mtx); // 等待条件成立队列非空。wait会原子地解锁锁并阻塞线程。 data_cond.wait(lock, []{ return !data_queue.empty(); }); // 被唤醒后锁已被重新获取且条件队列非空为真。 int value data_queue.front(); data_queue.pop(); lock.unlock(); // 可以提前解锁处理数据时不需持有锁 std::cout Consumed: value std::endl; if (value 9) break; // 简单退出条件 } }关键点解析wait的第一个参数是一个std::unique_lock因为wait内部需要解锁和重新加锁。wait的第二个参数是一个可调用对象这里用了lambda它返回一个布尔值。wait会在阻塞前和每次被唤醒后检查这个条件。这是为了防止“虚假唤醒”——即线程可能在没有收到notify的情况下被唤醒由于系统调度等原因。通过检查条件我们确保了被唤醒时条件确实满足。有notify_one()唤醒一个等待线程和notify_all()唤醒所有等待线程。根据业务逻辑选择。5.2 期值与承诺异步结果的传递std::future和std::promise以及std::async提供了一种更高级别的线程间通信机制用于在线程间传递异步操作的结果或异常。std::promise承诺提供一个值或异常。你可以把它看作一个值的生产者。std::future未来某个时刻获取一个值。你可以把它看作一个值的消费者。void do_work(std::promiseint result_promise) { std::this_thread::sleep_for(1s); try { int result 42; // 模拟计算结果 result_promise.set_value(result); // 履行承诺设置值 } catch (...) { result_promise.set_exception(std::current_exception()); // 传递异常 } } int main() { std::promiseint prom; std::futureint fut prom.get_future(); // 从承诺获取未来 std::thread worker(do_work, std::move(prom)); // 将承诺移动到线程中 // 在主线程做其他事情... std::cout Waiting for result...\\n; // 获取结果会阻塞直到结果就绪 int result fut.get(); // 如果do_work中设置了异常这里会抛出 std::cout Result: result std::endl; worker.join(); return 0; }std::async更便捷的异步任务对于简单的异步计算使用std::async比手动组合thread、promise、future更简单。int compute_something() { std::this_thread::sleep_for(1s); return 100; } int main() { // 启动一个异步任务 // std::launch::async 表示在新线程中执行 // std::launch::deferred 表示延迟执行直到调用get()时才在当前线程执行 std::futureint fut std::async(std::launch::async, compute_something); // ... 做其他事情 ... int result fut.get(); // 获取结果必要时等待 std::cout result std::endl; return 0; }注意事项std::future::get()只能调用一次调用后future状态变为无效。如果需要多个地方等待同一个结果可以使用std::shared_future。另外std::async的默认启动策略不指定std::launch是由实现定义的可能异步也可能延迟执行。如果明确需要异步最好指定std::launch::async。6. 无锁编程初探与原子操作的高级应用当锁成为性能瓶颈时一些开发者会转向更激进的无锁Lock-Free编程。无锁数据结构通过原子操作和内存序直接操作共享数据避免了锁的阻塞和上下文切换开销。但请注意无锁编程的难度和出错风险极高通常只用于性能极其敏感的底层组件。6.1 自旋锁一个简单的无锁实为忙等待锁例子自旋锁Spinlock在获取锁时不会让线程睡眠阻塞而是通过循环自旋不断尝试直到成功。它适用于锁持有时间非常短的场景。class spinlock { std::atomic_flag flag ATOMIC_FLAG_INIT; // 一个简单的原子布尔标志 public: void lock() { while (flag.test_and_set(std::memory_order_acquire)) { // 尝试设置标志为true // 获取失败自旋等待 // 可以加入 yield 或 pause 指令以减少CPU占用 // std::this_thread::yield(); // __builtin_ia32_pause(); // x86平台特定指令 } } void unlock() { flag.clear(std::memory_order_release); // 清除标志 } };std::atomic_flag是C中最简单的原子类型保证是无锁的。test_and_set是读-改-写操作它原子地读取当前值并设置为true。如果它返回false说明锁之前是自由的当前线程成功获取锁如果返回true说明锁已被占用继续循环。重要提醒自旋锁在单核CPU上通常是个坏主意会浪费整个时间片并且在锁竞争激烈或持有时间长的场景下会严重浪费CPU。它通常用于操作系统内核或极低延迟的库中。在用户态程序优先考虑std::mutex它通常已经过高度优化在竞争时会进行适当的休眠。6.2 无锁栈一个经典案例下面是一个极度简化的无锁栈单生产者单消费者场景下相对安全的push操作示例用于展示原子操作和内存序的配合templatetypename T class lock_free_stack { private: struct node { T data; node* next; node(const T data) : data(data), next(nullptr) {} }; std::atomicnode* head; public: void push(const T data) { node* new_node new node(data); new_node-next head.load(std::memory_order_relaxed); // 使用compare_exchange_weak循环来原子地更新head while (!head.compare_exchange_weak(new_node-next, new_node, std::memory_order_release, std::memory_order_relaxed)) { // 如果head不等于new_node-next被其他线程修改了 // compare_exchange_weak会自动将head的当前值更新到new_node-next中 // 然后循环继续尝试。 } } // pop操作更复杂涉及ABA问题等此处省略。 };compare_exchange_weak/compare_exchange_strong是无锁编程的核心操作。它原子地比较原子对象的值与期望值如果相等则用新值替换它如果不相等则将原子对象的当前值加载到期望值中。这是一个读-改-写操作。weak版本可能在某些架构上出现伪失败spurious failure但通常性能更好在循环中使用时没问题。严重警告上面的栈示例省略了pop、内存回收ABA问题等复杂部分绝对不能直接用于生产环境。无锁数据结构的正确实现极其困难涉及内存模型、ABA问题、安全的内存回收在C中常借助std::shared_ptr或引用计数等深水区。除非你是专家且有极强的需求否则请使用标准库或成熟的三方库中的并发容器如moodycamel::ConcurrentQueue。7. 实战避坑指南与性能考量理论学得再多不如踩一次坑记得牢。这里分享几个我在实际项目中遇到的典型问题和性能调优经验。7.1 锁竞争与性能瓶颈识别并发程序性能上不去第一个要怀疑的就是锁竞争。你可以通过以下方式初步判断Profiling工具使用像perf、VTune或valgrind --tooldrd这样的工具查看热点和锁争用情况。简单观察如果线程数增加到CPU核心数附近时程序吞吐量不再增长甚至下降很可能存在锁竞争或伪共享。优化策略缩小临界区再次检查锁住的代码只保护必要的数据操作。使用读写锁如果确实是读多写少将std::mutex替换为std::shared_mutex。数据分片将共享数据拆分成多个独立的部分用不同的锁保护例如一个哈希表可以用多个桶每个桶有自己的锁。无锁数据结构在确认锁是瓶颈且有能力驾驭时考虑使用无锁队列等。7.2 伪共享看不见的性能杀手现代CPU的缓存是以缓存行Cache Line通常64字节为单位的。如果两个频繁修改的变量位于同一个缓存行且被不同CPU核心使用就会导致“伪共享”。一个核心修改了变量A会导致整个缓存行失效迫使另一个核心即使只读变量B也要重新从内存加载缓存行造成大量不必要的缓存同步开销。// 不好的例子两个高度竞争的计数器放在一起 struct BadAlignment { int counter1; // 线程A频繁修改 int counter2; // 线程B频繁修改 // 假设int是4字节它们很可能在同一个64字节缓存行内 }; // 好的做法使用缓存行对齐进行填充 struct alignas(64) GoodAlignment { // C11 alignas 指定对齐 int counter1; char padding[60]; // 填充到大约一个缓存行大小 }; struct alignas(64) AnotherGood { int counter2; char padding[60]; }; // 或者使用编译器扩展如 __attribute__((aligned(64))) (GCC/Clang)对于高度竞争的数据确保它们位于不同的缓存行是提升多线程性能的一个有效技巧。7.3std::atomic不是万能的原子变量虽然强大但不能替代所有锁。例如你需要保护一个需要多个操作保持原子性的复杂数据结构比如双向链表插入节点单纯用原子变量很难正确实现。此时锁仍然是更简单、更安全的选择。记住正确性永远优先于性能。先用锁实现正确逻辑通过剖析证明锁是瓶颈后再考虑更复杂的优化。7.4 线程安全与STL容器标准库中的大多数容器如std::vector,std::map本身不是线程安全的。多个线程同时读写一个容器而不加锁会导致未定义行为通常是崩溃。唯一的例外是只读访问多个线程同时进行只读操作是安全的。const成员函数从用户角度看调用const成员函数应该是只读的但前提是用户没有通过其他方式如持有非常量指针或引用进行修改。如果需要并发容器可以考虑使用锁包装普通容器。使用TBBIntel Threading Building Blocks或FollyFacebook等库提供的并发容器。C标准库目前C23还没有通用的线程安全容器但有一些特例如std::shared_ptr和std::weak_ptr的引用计数操作是线程安全的但指向的对象本身不是。8. 工具链与调试技巧工欲善其事必先利其器。并发编程的调试比单线程困难得多因为bug可能时隐时现条件竞争。8.1 编译器与标准支持确保你的编译器支持所需的C标准C11/14/17/20并开启相关标志。例如在GCC/Clang中# 使用C17标准启用所有警告生成调试信息 g -stdc17 -Wall -Wextra -g -pthread your_program.cpp -o your_program-pthread链接线程库非常重要。8.2 线程消毒剂ThreadSanitizer这是并发调试的神器。ThreadSanitizer (TSan) 是一个数据竞争检测器。它能在运行时发现多线程访问共享数据时缺少同步的问题。# 使用GCC或Clang编译时启用TSan g -stdc17 -fsanitizethread -g -O1 your_program.cpp -o your_program_tsan运行程序如果存在数据竞争TSan会输出详细的报告指出冲突的代码位置和调用栈。注意TSan会显著降低程序运行速度并增加内存消耗仅用于调试。8.3 锁争用分析工具像valgrind --tooldrd或helgrind可以检测锁顺序问题、死锁等。一些IDE如Visual Studio、CLion也内置了并发调试和性能分析工具。8.4 日志与断言在并发程序中有策略地添加日志注意日志输出本身也要线程安全最好用类似spdlog这样的线程安全日志库可以帮助理解线程间的执行顺序。使用断言assert检查不变量invariants在临界区内外是否保持可以在开发早期发现问题。调试并发bug的核心思路是尝试让问题稳定复现。可以尝试增加循环次数、在可疑代码附近插入小的随机延迟std::this_thread::sleep_for、或者使用TSan这样的工具。一旦复现就成功了一大半。我个人在经历了许多并发项目的锤炼后最大的体会是并发编程的第一要务是建立清晰的数据所有权和访问边界思维。在动笔写多线程代码之前先画一画线程和数据流的关系图明确哪些数据是线程私有的哪些是共享的共享数据通过何种方式消息队列、原子变量、锁保护的结构进行通信。设计阶段多花一小时调试阶段可能就能节省好几天。对于《C并发编程实战》这本书前几章关于内存模型和原子操作的内容虽然艰涩但反复阅读和实践是值得的它们是你构建正确并发程序的地基。后面的高级主题如并行算法、线程池等都是在这个地基上搭建的建筑。先把地基打牢后面的路会顺畅很多。