C++并发编程实战:从进程线程原理到线程池实现

发布时间:2026/7/21 4:57:05
C++并发编程实战:从进程线程原理到线程池实现 1. 项目概述为什么并发编程是C开发者的必修课如果你写过C程序大概率遇到过这样的场景一个数据处理任务面对海量文件单线程跑起来要几个小时界面卡死用户疯狂点击。或者一个网络服务器只能同时服务一个客户端其他请求只能排队等待。这些问题的根源都指向了“顺序执行”的局限性。而解决之道就是并发编程。这不是什么高深莫测的黑魔法而是现代软件开发尤其是C高性能领域必须掌握的核心技能。简单来说并发编程就是让程序“同时”做多件事情。这里的“同时”需要打引号因为在单核CPU上是多个任务快速切换营造出同时的假象并发在多核CPU上才是真正的物理层面同时执行并行。无论是哪种其目的都是为了充分利用计算资源提升程序的吞吐量和响应速度。进程和线程就是实现并发的两个最基本、最重要的抽象模型。很多人对它们的区别停留在“进程是资源分配单位线程是CPU调度单位”这句教科书定义上但一到实际编码就懵什么时候用进程什么时候用线程C里怎么创建和管理它们锁该怎么用才不出错线程池又是个什么“池子”这篇文章我就以一个踩过无数坑的C老手的视角带你从零开始彻底搞懂进程与线程。我们不空谈理论而是结合C11/14/17标准库提供的现代工具如std::thread,std::async,std::mutex等通过实战代码把概念落地。你会看到如何用几行代码启动一个线程也会深入探讨如何安全地在多个线程间共享数据最后亲手搭建一个简易却地道的线程池。目标是让你读完就能在项目中用起来并且明白背后的“为什么”从而有能力设计出正确、高效的并发结构。2. 核心概念拆解进程、线程与并发模型在深入代码之前我们必须把地基打牢。进程和线程的概念如果混淆后续的所有设计都可能建立在流沙之上。2.1 进程独立的“王国”你可以把一个进程想象成一个独立的王国。这个王国有自己专属的疆域内存地址空间包括代码区、数据区、堆栈等。国王操作系统为每个王国分配资源王国之间有着明确的边界通过虚拟内存技术实现隔离。一个王国里的变故比如内存访问越界导致崩溃通常不会直接影响其他王国。这就是进程的核心特性隔离性。资源拥有者进程是系统进行资源分配如内存、I/O设备、文件句柄和保护的基本单位。开销大创建一个新进程fork或CreateProcess代价高昂需要复制或设置大量的内核数据结构如页表、文件描述符表等。通信复杂正因为隔离性好王国之间想传递消息进程间通信IPC就比较麻烦需要借助操作系统提供的特殊渠道比如管道、消息队列、共享内存等这些操作通常比线程间通信慢。在C中直接创建进程通常依赖于平台API如POSIX的fork()或Windows的CreateProcess标准库本身不提供直接创建进程的抽象。这通常用于需要强隔离性的场景比如启动一个完全独立的第三方程序。2.2 线程王国内的“工匠”线程则是同一个王国内部的工匠。所有工匠共享王国的所有资源同一片内存空间、同一个文件列表。他们在一个共同的蓝图代码下工作但各自有独立的工作台栈和当前的工作进度程序计数器、寄存器状态。工匠之间协作非常方便可以直接交流、共用工具共享数据但风险也随之而来如果一个工匠不小心弄乱了共享的工具数据竞争所有工匠的工作都可能出错。执行单元线程是CPU调度和执行的基本单位。一个进程至少有一个线程主线程。开销小创建和销毁线程的代价远小于进程因为大部分资源内存、文件等都是共享的无需复制。通信高效线程间通信极其简单直接读写共享内存即可。但这把双刃剑也带来了同步的难题。缺乏隔离一个线程崩溃如非法指针访问很可能导致整个进程崩溃因为地址空间是共享的。C11标准库引入的thread头文件正是为了让我们能以可移植的方式创建和管理这些“工匠”。2.3 并发与并行目标与能力这两个词经常被混用但严格来说有区别并发指在一段时间内多个任务都在向前推进。哪怕只有一个CPU核心通过时间片轮转也能实现并发。它关注的是程序的设计模型即处理多个逻辑上同时存在的任务流的能力。并行指在同一时刻有多个任务在同时执行。这要求硬件必须有多核或多CPU。并行是并发的一种物理实现方式能真正提升吞吐量。对于C开发者我们首先关注的是写出正确的并发程序逻辑正确处理好同步。至于这个程序在单核上并发执行还是在多核上并行执行通常由操作系统和硬件决定。当然我们可以通过std::thread::hardware_concurrency()来查询建议的线程数以更好地利用并行能力。注意一个常见的误解是“线程数等于CPU核心数时性能最优”。这并不绝对。对于计算密集型任务这通常是个不错的起点。但对于I/O密集型任务如网络请求、文件读写线程在等待I/O时会被阻塞此时创建比核心数更多的线程可能更能充分利用CPU。最佳线程数需要结合实际任务特性进行测试和调优。3. C标准库中的线程实战理论说再多不如一行代码。C11让多线程编程从平台相关的苦差事变成了可移植的现代编程体验。我们直接从最基础的std::thread开始。3.1 创建与等待线程假设我们有一个简单的函数用来计算一个数的平方#include iostream #include thread #include chrono void calculate_square(int x) { std::this_thread::sleep_for(std::chrono::milliseconds(500)); // 模拟耗时计算 std::cout The square of x is x * x std::endl; } int main() { std::cout Main thread ID: std::this_thread::get_id() std::endl; // 创建线程并立即执行calculate_square(5) std::thread worker(calculate_square, 5); std::cout Worker thread ID: worker.get_id() std::endl; // 主线程继续执行其他工作 std::cout Main thread is doing other work... std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(200)); // 等待worker线程执行完毕 worker.join(); // 这是一个阻塞调用主线程会停在这里直到worker结束 std::cout Worker thread finished. Main thread exits. std::endl; return 0; }关键点解析构造即启动std::thread worker(func, args...);一旦创建新线程就会立即开始执行func。你无法创建一个“暂停”状态的标准线程对象。参数传递线程函数的参数是按值传递的。如果需要传递引用必须使用std::ref()进行包装例如std::thread t(func, std::ref(some_var))。传递指针时要极度小心生命周期问题。join()与detach()join()等待线程结束并清理相关资源。调用后thread对象不再关联任何线程。你必须确保在thread对象销毁前要么join()要么detach()否则程序会调用std::terminate()终止这是新手最容易犯的错误之一。detach()将线程与thread对象分离允许线程“在后台”独立运行。分离后你将失去对该线程的控制也无法再join()。通常用于守护线程或一次性任务。线程标识std::this_thread::get_id()可以获取当前线程的ID用于调试。3.2 使用Lambda表达式与可调用对象除了普通函数std::thread的构造函数非常灵活可以接受任何可调用对象。// 使用Lambda表达式 std::thread t1([](){ std::cout Hello from lambda thread! std::endl; }); // 使用函数对象仿函数 class Task { public: void operator()(int n) const { for(int i 0; i n; i) { std::cout Working... i std::endl; } } }; Task task; std::thread t2(task, 3); // 传递函数对象实例和参数 t1.join(); t2.join();使用Lambda尤其方便可以轻松捕获局部变量但要警惕生命周期。例如捕获局部变量int count时默认是值捕获[count]引用捕获是[count]。3.3 异步操作std::async与std::future有时候我们并不想手动管理线程的细节只是希望异步地执行一个任务并在未来某个时刻获取结果。std::async和std::future这对搭档就是为此而生。#include iostream #include future #include chrono int long_computation(int a, int b) { std::this_thread::sleep_for(std::chrono::seconds(2)); return a b; } int main() { // 启动一个异步任务 // std::launch::async 策略保证任务会在新线程中执行 // std::launch::deferred 则会延迟到调用get()时在调用者线程中同步执行 std::futureint result_future std::async(std::launch::async, long_computation, 10, 20); std::cout Main thread can do other work while computing... std::endl; std::this_thread::sleep_for(std::chrono::seconds(1)); // 获取结果。如果任务未完成get()会阻塞等待。 int result result_future.get(); // 这里可能会阻塞约1秒 std::cout The result is: result std::endl; // result_future.get() 只能调用一次第二次调用会导致异常。 return 0; }std::async的优势与陷阱优势抽象层次更高无需直接操作线程。可以方便地获取返回值或异常异常会在调用get()时被重新抛出。策略选择std::launch::async强制异步和std::launch::deferred延迟同步的行为差异巨大。默认策略是async|deferred由实现决定这可能导致不确定性。最佳实践是显式指定策略。隐藏的线程管理std::async返回的future在析构时如果策略是async它会等待任务完成。这意味着如果你不保存future对象异步任务会变成“即发即弃”但阻塞等待的模式可能引发意想不到的延迟。实操心得对于简单的“发射后不管”的任务std::thread更直观。对于需要获取结果、且希望由运行时库来优化线程管理的场景std::async是更好的选择。但在高性能服务器等需要精确控制线程行为的场景手动管理线程池仍然是主流。4. 线程同步数据竞争与保护利器多个线程共享数据是并发编程的威力所在也是万恶之源。不加保护的并发访问会导致数据竞争结果是未定义的程序崩溃、结果错误、诡异行为都可能发生。4.1 互斥锁std::mutex互斥锁是最基本的同步原语它保证同一时间只有一个线程能进入被保护的代码段临界区。#include iostream #include thread #include mutex #include vector std::mutex g_mutex; // 全局互斥锁 int shared_counter 0; void increment_with_mutex(int num_iterations) { for (int i 0; i num_iterations; i) { { // 使用RAII风格的锁管理器构造时加锁析构时自动解锁 std::lock_guardstd::mutex lock(g_mutex); // 临界区开始 int temp shared_counter; // 模拟一些操作增加竞争窗口 std::this_thread::yield(); shared_counter temp 1; // 临界区结束 } // lock_guard在此析构自动释放锁 // 锁外可以做一些非共享的操作 } } int main() { const int num_threads 10; const int iterations_per_thread 1000; std::vectorstd::thread threads; for (int i 0; i num_threads; i) { threads.emplace_back(increment_with_mutex, iterations_per_thread); } for (auto t : threads) { t.join(); } std::cout Expected counter value: num_threads * iterations_per_thread std::endl; std::cout Actual counter value: shared_counter std::endl; return 0; }关键点与进阶锁std::lock_guard最简单的RAII锁管理器。在构造时锁定mutex在析构时通常是在作用域结束时自动解锁。它不提供手动加解锁的接口防止程序员忘记解锁。std::unique_lock更灵活的RAII锁管理器。除了具备lock_guard的功能外还允许延迟加锁、手动加解锁、转移所有权等。通常与条件变量std::condition_variable配合使用。std::unique_lockstd::mutex lock(g_mutex, std::defer_lock); // 延迟加锁 // ... 做一些不需要锁的操作 lock.lock(); // 手动加锁 // 临界区 lock.unlock(); // 可以手动解锁在unique_lock析构前还可以重新加锁死锁当两个或以上线程互相等待对方持有的锁时就会发生死锁程序永远卡住。避免死锁的黄金法则固定顺序上锁所有线程都按相同的全局顺序获取锁例如总是先锁A再锁B。使用std::lock一次性锁定多个互斥量C标准库提供了std::lock(mutex1, mutex2, ...)它可以一次性锁定多个互斥量且保证不会死锁。通常配合std::lock_guard的std::adopt_lock标签使用。std::mutex mutex_a, mutex_b; // 错误做法可能导致死锁 // thread1: lock_a.lock(); lock_b.lock(); // thread2: lock_b.lock(); lock_a.lock(); // 正确做法 std::lock(mutex_a, mutex_b); // 一次性锁住两个无死锁风险 std::lock_guardstd::mutex lock_a(mutex_a, std::adopt_lock); std::lock_guardstd::mutex lock_b(mutex_b, std::adopt_lock);4.2 条件变量线程间的“信号灯”互斥锁解决了互斥访问但线程间经常需要协作一个线程需要等待某个条件成立例如任务队列非空才能继续执行。忙等待不断循环检查条件会浪费CPU。条件变量std::condition_variable提供了高效的等待/通知机制。#include iostream #include thread #include mutex #include condition_variable #include queue std::mutex mtx; std::condition_variable cv; std::queueint 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_guardstd::mutex lock(mtx); data_queue.push(i); std::cout Produced: i std::endl; } // 锁在通知前释放是良好实践 cv.notify_one(); // 通知一个等待的消费者 } { std::lock_guardstd::mutex lock(mtx); finished true; } cv.notify_all(); // 通知所有消费者结束 } // 消费者线程 void consumer(int id) { while (true) { std::unique_lockstd::mutex lock(mtx); // 等待条件队列非空或生产结束。防止虚假唤醒spurious wakeup必须用while循环检查条件。 cv.wait(lock, []{ return !data_queue.empty() || finished; }); if (finished data_queue.empty()) { break; // 生产结束且队列已空退出循环 } // 条件满足处理数据 int value data_queue.front(); data_queue.pop(); lock.unlock(); // 尽早释放锁让其他消费者可以继续 std::cout Consumer id consumed: value std::endl; // 处理数据... } std::cout Consumer id exited. std::endl; } int main() { std::thread prod(producer); std::thread cons1(consumer, 1); std::thread cons2(consumer, 2); prod.join(); cons1.join(); cons2.join(); return 0; }条件变量的使用模式等待端使用std::unique_lock在wait函数中传入锁和一个谓词返回bool的lambda或函数。wait会在阻塞前自动释放锁并在被唤醒后重新获取锁然后检查谓词。如果谓词为false它会继续等待。这个“检查-等待”循环是应对虚假唤醒操作系统可能无故唤醒等待的线程的标准做法。通知端在修改了条件相关的共享数据如data_queue或finished后调用notify_one()唤醒一个等待线程或notify_all()唤醒所有等待线程。最佳实践是在持有锁的情况下修改数据但在调用notify前释放锁这样被唤醒的线程能立即获取锁减少上下文切换。4.3 原子操作无锁编程的基石对于简单的计数器、标志位使用互斥锁可能杀鸡用牛刀开销过大。C11提供了std::atomic模板用于定义原子类型。对原子类型的操作是不可分割的因此是线程安全的无需额外的锁。#include iostream #include thread #include atomic #include vector std::atomicint atomic_counter{0}; // 原子计数器 void increment_atomic(int num_iterations) { for (int i 0; i num_iterations; i) { atomic_counter.fetch_add(1, std::memory_order_relaxed); // 原子加1 // 等价于 atomic_counter; (但操作符是顺序一致的内存序稍重) } } int main() { const int num_threads 10; const int iterations_per_thread 100000; std::vectorstd::thread threads; for (int i 0; i num_threads; i) { threads.emplace_back(increment_atomic, iterations_per_thread); } for (auto t : threads) { t.join(); } std::cout Atomic counter value: atomic_counter.load() std::endl; return 0; }原子操作的优势与内存序性能原子操作通常由CPU指令直接支持比互斥锁快得多。内存序这是原子操作最复杂也最重要的部分。std::memory_order指定了原子操作周围非原子内存访问的可见性顺序。memory_order_seq_cst顺序一致性默认选项最强约束保证所有线程看到的操作顺序一致。性能开销最大。memory_order_relaxed松散顺序只保证原子操作本身的原子性不提供线程间同步。性能最好但使用场景有限如简单的计数器。memory_order_acquire/memory_order_release/memory_order_acq_rel用于实现“释放-获取”语义是构建无锁数据结构的关键。一个线程release写入的数据能被另一个acquire读取的线程看到。建议除非你在进行极低延迟的无锁数据结构开发否则优先使用默认的memory_order_seq_cst。在确保正确性的前提下再考虑用更宽松的内存序进行性能优化。错误的内存序会导致极其隐蔽的并发Bug。5. 构建简易线程池管理并发工作负载频繁创建和销毁线程的代价很高。线程池是一种生产者-消费者模型的经典应用它预先创建一组线程并保持活跃将任务提交到队列中由池中的线程取出执行。这避免了线程生命周期的开销并能控制并发度。下面我们实现一个简易但功能完整的线程池。#include iostream #include vector #include queue #include thread #include mutex #include condition_variable #include functional #include future #include memory class ThreadPool { public: // 构造函数创建指定数量的工作线程 explicit ThreadPool(size_t thread_count std::thread::hardware_concurrency()) : stop(false) { for(size_t i 0; i thread_count; i) { workers.emplace_back([this] { for(;;) { // 无限循环直到线程池停止 std::functionvoid() task; { // 这个花括号限定了锁的作用域 std::unique_lockstd::mutex lock(this-queue_mutex); // 等待条件池子停止或任务队列非空 this-condition.wait(lock, [this]{ return this-stop || !this-tasks.empty(); }); // 如果池子已停止且任务已清空线程结束 if(this-stop this-tasks.empty()) { return; } // 取出一个任务 task std::move(this-tasks.front()); this-tasks.pop(); } // 锁的作用域结束自动释放锁 // 执行任务在锁外执行避免长时间持有锁 task(); } }); } } // 提交一个任务到线程池返回一个future以便获取结果 templateclass F, class... Args auto enqueue(F f, Args... args) - std::futuretypename std::result_ofF(Args...)::type { // 推导任务返回类型 using return_type typename std::result_ofF(Args...)::type; // 将任务和参数打包成一个无参数的void()函数并用shared_ptr管理 auto task std::make_sharedstd::packaged_taskreturn_type()( std::bind(std::forwardF(f), std::forwardArgs(args)...) ); // 获取与任务关联的future std::futurereturn_type res task-get_future(); { std::unique_lockstd::mutex lock(queue_mutex); // 不允许在停止的线程池中添加任务 if(stop) { throw std::runtime_error(enqueue on stopped ThreadPool); } // 将任务包装成void()函数放入队列 tasks.emplace([task](){ (*task)(); }); } // 通知一个等待的工作线程 condition.notify_one(); return res; } // 析构函数停止所有线程 ~ThreadPool() { { std::unique_lockstd::mutex lock(queue_mutex); stop true; } condition.notify_all(); // 唤醒所有线程 for(std::thread worker: workers) { worker.join(); // 等待所有线程结束 } } private: std::vectorstd::thread workers; // 工作线程集合 std::queuestd::functionvoid() tasks; // 任务队列 std::mutex queue_mutex; // 保护任务队列的互斥锁 std::condition_variable condition; // 用于线程等待的条件变量 bool stop; // 停止标志 }; // 使用示例 int main() { ThreadPool pool(4); // 创建4个线程的池子 std::vectorstd::futureint results; // 保存future的容器 // 提交8个任务 for(int i 0; i 8; i) { results.emplace_back( pool.enqueue([i] { std::cout Task i started by thread std::this_thread::get_id() std::endl; std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟工作 std::cout Task i finished. std::endl; return i * i; // 返回结果 }) ); } // 获取所有任务的结果 for(auto result: results) { std::cout Result: result.get() std::endl; } // ThreadPool对象析构时会自动等待所有任务完成并关闭线程 return 0; }线程池实现要点解析工作线程循环每个工作线程的核心是一个无限循环等待条件变量。当有任务或停止信号时被唤醒。任务封装enqueue方法使用std::packaged_task和std::bind将任意可调用对象及其参数打包成一个返回std::future的通用任务。这使得我们可以异步获取任务返回值。完美转发enqueue使用std::forward进行完美转发避免不必要的拷贝支持移动语义。资源管理任务队列使用std::functionvoid()存储用std::shared_ptr管理packaged_task的生命周期确保任务在执行前不会被销毁。优雅关闭析构函数设置stop标志并通知所有线程。线程在发现stop为true且任务队列为空后才会退出循环。这确保了所有已入队的任务都能被执行完。异常安全enqueue在池子已停止时抛出异常。任务执行中的异常会被捕获并存储到对应的future中在调用get()时重新抛出。注意事项这是一个基础的教学实现。生产环境的线程池需要考虑更多因素例如动态调整线程数量、支持优先级队列、处理线程中的未捕获异常、提供更丰富的关闭策略如立即关闭、等待当前任务完成等。C17/20的std::jthread可联结线程自带停止令牌和std::stop_token可以用于构建更优雅的停止机制。6. 常见并发问题与调试技巧实录即使理解了所有概念和工具并发编程依然容易出错。下面记录几个我实际开发中踩过的坑和解决方法。6.1 死锁的预防与诊断死锁的经典条件是四个互斥、持有并等待、不可剥夺、循环等待。预防死锁除了前面提到的固定顺序上锁和std::lock还有一些策略避免嵌套锁尽量缩小锁的粒度一个函数只持有一个锁。如果必须持有多个锁使用std::lock。使用层次锁为锁定义层次级别线程只能按从高到低的顺序获取锁。这可以在编码时通过自定义锁包装器实现。锁超时C提供了std::timed_mutex和std::recursive_timed_mutex可以使用try_lock_for尝试获取锁一段时间超时则执行备用逻辑如记录日志、重试或失败返回。但这增加了复杂度。诊断死锁当程序挂起时可以使用调试器如GDB查看所有线程的调用栈。如果发现多个线程都在__lll_lock_wait或类似的锁等待函数中并且它们等待的锁被对方持有基本可以断定是死锁。一些工具如helgrindValgrind的一部分也能在运行时检测潜在的死锁。6.2 数据竞争与竞态条件数据竞争是未定义行为的根源。使用std::mutex和原子操作可以避免数据竞争但竞态条件Race Condition更微妙即使每个单独的内存访问是原子的操作的组合顺序也可能因线程调度而产生错误结果。示例经典的“检查后行动”Check-Then-Act问题。std::vectorint vec; std::mutex vec_mutex; // 线程A if (!vec.empty()) { // 检查 std::lock_guardstd::mutex lock(vec_mutex); // 行动但太晚了 int value vec.back(); // 可能出错因为检查后vec可能被线程B修改 vec.pop_back(); } // 线程B可能在线程A检查之后、加锁之前清空了vec。解决方法将“检查”和“行动”作为一个原子操作保护在同一个锁下。std::lock_guardstd::mutex lock(vec_mutex); // 先加锁 if (!vec.empty()) { // 在锁的保护下检查 int value vec.back(); vec.pop_back(); }工具辅助编译器标志-fsanitizethreadGCC/Clang可以在编译时插入检测代码在运行时发现数据竞争。valgrind --toolhelgrind也是强大的动态分析工具。6.3 虚假唤醒与条件变量使用范式前面条件变量的例子已经展示了标准范式wait必须配合一个谓词Predicate在循环中检查。这是因为即使没有线程调用notify等待的线程也可能被操作系统唤醒虚假唤醒。循环检查确保了条件真正满足后才继续执行。错误模式// 错误可能因虚假唤醒而访问空队列 std::unique_lockstd::mutex lock(mtx); if (data_queue.empty()) { cv.wait(lock); // 如果虚假唤醒会直接往下走但队列仍是空的 } // 这里直接使用data_queue.front()会导致未定义行为正确模式std::unique_lockstd::mutex lock(mtx); while (data_queue.empty()) { // 或者用带谓词的wait cv.wait(lock); } // 或者更简洁的 cv.wait(lock, []{ return !data_queue.empty(); });6.4 线程局部存储thread_local有些数据你希望每个线程都有一份独立的副本比如随机数生成器、数据库连接、或者一些上下文信息。C11引入了thread_local关键字。thread_local int thread_specific_value 0; // 每个线程都有自己独立的副本 void thread_func(int id) { thread_specific_value id; // 修改只影响本线程 std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::cout Thread id : value thread_specific_value std::endl; } int main() { std::thread t1(thread_func, 1); std::thread t2(thread_func, 2); thread_specific_value 100; // 主线程的副本 t1.join(); t2.join(); std::cout Main thread: value thread_specific_value std::endl; // 输出可能是 // Thread 1: value 1 // Thread 2: value 2 // Main thread: value 100 }thread_local变量在线程启动时初始化在线程结束时销毁。对于非POD类型需要注意初始化的线程安全性和销毁顺序。6.5 性能考量锁粒度与无锁数据结构锁是性能瓶颈。减少锁竞争的方法细化锁粒度用多个细粒度锁保护不同的数据而不是一个大锁保护所有。但要小心死锁。缩短持锁时间在锁内只做必要的操作将耗时操作如I/O、复杂计算移到锁外。使用读写锁C14提供了std::shared_timed_mutexC17提供了std::shared_mutex。允许多个线程并发读但写独占。适用于读多写少的场景。考虑无锁数据结构对于性能极其敏感的场景可以考虑无锁队列、无锁栈等。它们完全基于原子操作和内存序避免了锁的开销但实现极其复杂且正确性难以保证。除非确有必要并且你对此有深入研究否则建议使用成熟的第三方库如moodycamel::ConcurrentQueue。我个人在项目中会先用清晰的、带锁的设计实现功能通过性能测试定位热点。如果锁竞争确实是瓶颈再考虑上述优化策略。盲目追求无锁往往会导致代码复杂且充满Bug。