C++11多线程同步实战:互斥锁、条件变量、原子操作与异步编程详解

发布时间:2026/8/25 11:47:54
C++11多线程同步实战:互斥锁、条件变量、原子操作与异步编程详解 1. 从单线程到多线程为什么同步是绕不开的坎十年前我刚接触C时写程序基本就是一条路走到黑。一个main函数从上到下执行逻辑清晰世界和平。直到有一天我需要写一个简单的网络服务要同时处理多个客户端的连接请求。用单线程写了个select模型虽然能用但性能瓶颈很快就出现了——当一个客户端在进行耗时的文件传输时其他所有客户端都得干等着。那一刻我意识到是时候拥抱多线程了。C在C11标准之前多线程编程是个“各显神通”的领域你得依赖pthread这样的平台特定库代码写起来既啰嗦又难以移植。C11将多线程支持纳入了标准库这绝对是一个里程碑。它提供了std::thread、std::mutex、std::condition_variable等一系列工具让我们能用标准、可移植的方式编写并发程序。然而多线程带来的不仅仅是性能提升更带来了一个核心挑战数据竞争和执行顺序的不确定性。想象一下两个线程同时去修改一个全局的计数器变量或者一个线程正在读取一个数据结构而另一个线程却在修改它结果将是不可预测的程序可能会崩溃也可能产生诡异且难以复现的错误结果。这就是我们需要“同步”机制的根本原因——为多个线程间的协作制定规则确保它们能有序、安全地访问共享资源。今天我们就来深入聊聊C11中提供的几把解决多线程同步问题的“瑞士军刀”互斥锁、条件变量、异步操作和原子操作。信号量虽然经典但在C标准库中并未直接提供我们也会探讨其原因和替代方案。无论你是正在准备多线程面试题的求职者还是被Qt多线程、Java多线程搞得有点晕想看看C怎么做的开发者抑或是好奇python中的多线程为何有GIL限制的同学理解这些底层同步原语都能让你对并发编程有更本质的认识。我们会避开枯燥的理论说教用实际的代码例子和踩坑经验把这些概念讲清楚。2. 互斥锁共享资源的“独木桥”互斥锁是多线程同步中最基础、最常用的工具。它的思想很简单对于一段需要访问共享资源的代码称为临界区一次只允许一个线程进入。这就像一座独木桥同一时间只能有一个人通过其他人必须等待。2.1 std::mutex 的基本用法C11提供了std::mutex类。使用它通常分为三步加锁、访问共享资源、解锁。#include iostream #include thread #include mutex #include vector std::mutex g_mutex; // 全局互斥锁 int shared_counter 0; void increment_counter(int num_iterations) { for (int i 0; i num_iterations; i) { g_mutex.lock(); // 进入临界区前加锁 // --- 临界区开始 --- int current_value shared_counter; // 模拟一点点耗时操作增加数据竞争出现的概率 std::this_thread::sleep_for(std::chrono::microseconds(1)); shared_counter current_value 1; // --- 临界区结束 --- g_mutex.unlock(); // 离开临界区后解锁 } } int main() { std::thread t1(increment_counter, 1000); std::thread t2(increment_counter, 1000); t1.join(); t2.join(); std::cout Final counter value: shared_counter std::endl; // 正确输出 2000 return 0; }这个例子中两个线程各对shared_counter增加1000次。如果没有g_mutex.lock()和unlock()的保护由于current_value shared_counter和shared_counter current_value 1不是原子操作最终结果很可能小于2000。互斥锁确保了整个“读-改-写”过程的完整性。2.2 为什么推荐使用 std::lock_guard 和 std::unique_lock直接使用lock()和unlock()是非常危险的因为如果在临界区中发生了异常或者程序员忘记调用unlock()就会导致锁永远无法释放其他所有等待该锁的线程都会被永久阻塞这就是臭名昭著的“死锁”问题之一。注意手动管理锁的获取和释放是极易出错的应作为反面教材。现代C强调RAII资源获取即初始化思想让对象的生命周期来管理资源。std::lock_guard在构造时自动加锁在析构时自动解锁。适用于简单的临界区保护。void safe_increment(int num_iterations) { for (int i 0; i num_iterations; i) { std::lock_guardstd::mutex lock(g_mutex); // 构造时加锁 int current_value shared_counter; std::this_thread::sleep_for(std::chrono::microseconds(1)); shared_counter current_value 1; } // lock_guard 析构自动解锁 }lock_guard简单、轻量但它只能在析构时解锁锁的持有周期就是其作用域。std::unique_lock比lock_guard更灵活。它允许延迟加锁、提前解锁并且可以转移所有权。它是实现条件变量等待所必需的。std::mutex mtx; std::queueint data_queue; void data_processor() { while (true) { std::unique_lockstd::mutex lock(mtx); // 加锁 if (data_queue.empty()) { lock.unlock(); // 可以手动提前解锁让其他线程能操作队列 std::this_thread::yield(); // 让出CPU时间片 continue; } int data data_queue.front(); data_queue.pop(); lock.unlock(); // 处理数据前就可以解锁减少锁的持有时间 process(data); // 假设process是耗时的 } }unique_lock的灵活性是以微小的性能开销为代价的在不需要条件变量或特殊锁管理的情况下优先使用lock_guard。2.3 死锁当多把锁成为“哲学家就餐问题”死锁是互斥锁使用中最棘手的问题。一个经典的场景是线程需要同时获取多把锁。std::mutex mtx1, mtx2; void thread_a() { std::lock_guardstd::mutex lock1(mtx1); std::this_thread::sleep_for(std::chrono::milliseconds(1)); // 增加死锁概率 std::lock_guardstd::mutex lock2(mtx2); // 等待mtx2但mtx2被thread_b持有 // ... 操作需要mtx1和mtx2保护的资源 } void thread_b() { std::lock_guardstd::mutex lock2(mtx2); std::this_thread::sleep_for(std::chrono::milliseconds(1)); std::lock_guardstd::mutex lock1(mtx1); // 等待mtx1但mtx1被thread_a持有 // ... 死锁发生 }线程A持有mtx1请求mtx2线程B持有mtx2请求mtx1。双方都握着对方想要的资源不放程序就此卡死。解决方案1固定锁的顺序所有线程都按照相同的全局顺序获取锁例如总是先锁mtx1再锁mtx2。这需要你在设计时规划好。解决方案2使用 std::lock 一次性锁定多个互斥量C11提供了std::lock函数它可以一次性锁定两个或更多的互斥量且不会产生死锁内部通常使用类似“尝试-回退”的算法。void safe_thread_a() { // std::lock 会一次性锁定mtx1和mtx2避免因中间穿插导致的死锁 std::lock(mtx1, mtx2); // 但lock之后互斥量仍处于锁定状态需要用lock_guard/adopt_lock接管 std::lock_guardstd::mutex lock1(mtx1, std::adopt_lock); std::lock_guardstd::mutex lock2(mtx2, std::adopt_lock); // ... 安全操作 }std::adopt_lock参数告诉lock_guard互斥量已经被当前线程锁定了lock_guard只需要在析构时负责解锁即可。解决方案3避免嵌套锁重新设计代码结构尽量减少需要同时持有多个锁的场景。例如可以将需要多个锁保护的操作提取到一个函数中并由一个全局锁来保护这个函数的调用。3. 条件变量让线程学会“等待”与“通知”互斥锁解决了“互斥访问”的问题但有时候线程需要等待某个条件成立才能继续执行。比如消费者线程需要等待队列不为空。如果只用互斥锁消费者线程可能会陷入“忙等待”的循环不停地加锁、检查队列、解锁、睡眠片刻这非常浪费CPU资源。条件变量std::condition_variable就是用来解决这个问题的。它允许一个线程在条件不满足时主动释放锁并进入等待状态直到其他线程改变了条件并通知它。3.1 生产者-消费者模型的标准实现这是条件变量最经典的应用场景。#include iostream #include thread #include mutex #include condition_variable #include queue std::mutex mtx; std::condition_variable cv; std::queueint data_queue; const int MAX_QUEUE_SIZE 10; bool production_done false; // 生产结束标志 void producer(int id) { for (int i 0; i 20; i) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟生产耗时 std::unique_lockstd::mutex lock(mtx); // 等待条件队列未满。如果满了就释放锁并等待。 cv.wait(lock, []{ return data_queue.size() MAX_QUEUE_SIZE; }); data_queue.push(i); std::cout Producer id produced: i std::endl; lock.unlock(); // 通知前可以解锁减少竞争 cv.notify_all(); // 通知所有等待的消费者也可以notify_one } // 生产完毕 std::lock_guardstd::mutex lock(mtx); production_done true; cv.notify_all(); } void consumer(int id) { while (true) { std::unique_lockstd::mutex lock(mtx); // 等待条件队列不空或生产已结束。注意lambda表达式。 cv.wait(lock, []{ return !data_queue.empty() || production_done; }); // 被唤醒后需要重新检查条件。因为可能是生产结束的通知。 if (production_done data_queue.empty()) { break; // 生产结束且队列已空消费者退出 } // 条件满足消费数据 int data data_queue.front(); data_queue.pop(); std::cout Consumer id consumed: data std::endl; lock.unlock(); // 消费完成解锁 cv.notify_all(); // 通知可能正在等待“队列未满”的生产者 } std::cout Consumer id exited. std::endl; } int main() { std::thread p1(producer, 1); std::thread c1(consumer, 1); std::thread c2(consumer, 2); p1.join(); c1.join(); c2.join(); return 0; }3.2 理解 cv.wait 的“双重检查”与虚假唤醒cv.wait(lock, predicate)是条件变量使用的核心。它等价于while (!predicate()) { cv.wait(lock); }这个while循环至关重要它解决了两个问题条件重检当线程被notify唤醒时条件如!data_queue.empty()可能已经再次变为假。例如多个消费者被同时唤醒第一个消费者拿走了数据队列又空了。如果没有重检第二个消费者就会错误地尝试从空队列取数据。虚假唤醒在某些操作系统实现中即使没有线程调用notify等待的线程也可能被无缘无故地唤醒。这是POSIX标准和C标准都允许的行为。使用while循环可以确保线程只在条件真正满足时才继续执行。实操心得永远使用带谓词predicate的wait重载版本。手写while循环容易出错而cv.wait(lock, []{return condition;})这种写法既安全又清晰。谓词那个lambda函数应该只检查条件不要在里面进行任何有副作用的操作。3.3 std::condition_variable_any标准库还提供了std::condition_variable_any它可以和任何满足BasicLockable概念即有lock()和unlock()方法的锁一起工作而std::condition_variable只能和std::unique_lockstd::mutex配合。condition_variable_any更通用但性能可能稍差。在绝大多数使用std::mutex的场景下用std::condition_variable就够了。4. 异步操作让任务在后台“飞一会儿”有时候我们并不关心线程间严格的执行顺序只是希望一些耗时的操作不要阻塞主线程。比如在GUI程序中进行一个网络请求或复杂计算。C11的future库提供了一套高级的异步操作工具让我们可以更方便地启动后台任务并获取结果。4.1 std::async 与 std::futurestd::async是一个函数模板它尝试启动一个异步任务可能在新线程中也可能在后续同步执行。它返回一个std::future对象用于获取异步任务的结果。#include iostream #include future #include chrono #include numeric #include vector // 一个耗时的计算函数 long long calculate_sum(const std::vectorint data) { std::cout Async task started on thread: std::this_thread::get_id() std::endl; std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时 return std::accumulate(data.begin(), data.end(), 0LL); } int main() { std::vectorint big_data(10000000, 1); // 一千万个1 // 使用 std::async 启动异步任务 // std::launch::async 策略强制在新线程中执行 // std::launch::deferred 策略延迟执行直到调用future.get()时才在当前线程执行 // 默认策略(std::launch::async | std::launch::deferred)由实现决定 std::futurelong long future_result std::async(std::launch::async, calculate_sum, big_data); std::cout Main thread is doing other work... std::endl; std::this_thread::sleep_for(std::chrono::seconds(1)); std::cout Main thread work done, waiting for result... std::endl; // 获取结果。如果任务未完成会阻塞直到完成。 long long result future_result.get(); std::cout The sum is: result std::endl; return 0; }std::future::get()方法会阻塞调用线程直到异步任务完成并返回结果。一个future只能调用一次get()。4.2 std::packaged_task 与 std::promisestd::async很方便但有时我们需要更精细的控制比如想将任务提交到特定的线程池。这时可以用std::packaged_task和std::promise。std::packaged_task将一个可调用对象包装起来使其可以异步执行并允许获取一个与之关联的future。#include future #include iostream #include thread int heavy_computation(int x) { std::this_thread::sleep_for(std::chrono::seconds(1)); return x * x; } int main() { // 将函数包装成packaged_task std::packaged_taskint(int) task(heavy_computation); // 获取与任务关联的future std::futureint result task.get_future(); // 将任务移动到另一个线程中执行 std::thread t(std::move(task), 10); t.detach(); // 分离线程或者用join // 在主线程做其他事... std::cout Waiting for result... std::endl; std::cout Result is: result.get() std::endl; // 阻塞并获取结果 return 0; }packaged_task本身就是一个可调用对象你可以把它放入队列由工作线程取出执行这是实现线程池的常见方式。std::promise提供了一个“承诺”可以在未来某个时刻设置一个值或异常并通过与之关联的future来获取这个值。它用于在线程间传递结果尤其适合那些无法用简单返回值沟通的场景比如需要传递多个结果或者结果产生的时机非常特殊。void do_work(std::promiseint result_promise) { try { std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟可能失败的操作 // if (something_bad) throw std::runtime_error(Oops!); result_promise.set_value(42); // 履行承诺设置值 } catch (...) { // 捕获任何异常并通过promise传递出去 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 the promise... std::endl; try { int value fut.get(); // 阻塞直到promise被set_value或set_exception std::cout Value received: value std::endl; } catch (const std::exception e) { std::cout Exception from worker: e.what() std::endl; } worker.join(); return 0; }4.3 异步操作的异常处理std::async、packaged_task和promise都能很好地传递异常。在异步任务中抛出的异常会在调用future.get()时在调用线程中重新抛出。这让我们能用同步代码的风格来处理异步错误非常强大。踩坑提醒std::async在默认启动策略下析构返回的future时可能会阻塞等待任务完成如果任务是以std::launch::async策略启动的。如果你不关心任务结果又不想阻塞要么用std::launch::deferred要么把future保存起来或者用其他方式管理任务生命周期。这也是为什么对于“即发即弃”的任务有时人们会选择直接创建std::thread。5. 原子操作无需锁的“最小同步”互斥锁是重量级的同步原语因为它涉及到操作系统的系统调用和线程的挂起/唤醒开销不小。对于一些简单的共享变量操作比如一个计数器我们有没有更轻量级的选择原子操作就是答案。原子操作指的是不可被中断的一个或一系列操作。对于其他线程来说一个原子操作要么完全执行完毕要么完全没执行看不到中间状态。C11在atomic头文件中提供了一系列原子类型。5.1 std::atomic 的基本使用最常用的就是std::atomicT其中T可以是整型、指针类型甚至在某些条件下是用户自定义类型需满足可平凡复制等条件。#include atomic #include thread #include iostream #include vector std::atomicint atomic_counter{0}; // 原子计数器 // int raw_counter 0; // 对比非原子计数器 void atomic_increment(int n) { for (int i 0; i n; i) { atomic_counter.fetch_add(1, std::memory_order_relaxed); // 原子加1 // atomic_counter; // 等价且更简洁 } } int main() { const int num_threads 10; const int increments_per_thread 100000; std::vectorstd::thread threads; for (int i 0; i num_threads; i) { threads.emplace_back(atomic_increment, increments_per_thread); } for (auto t : threads) { t.join(); } std::cout Atomic counter final value: atomic_counter std::endl; // 正确输出 1000000 return 0; }即使有10个线程同时疯狂累加atomic_counter的最终结果也一定是正确的100万。如果换成普通的int raw_counter结果会远小于100万因为raw_counter这个操作在汇编层面是“读取-修改-写回”三步不是原子的。5.2 内存序原子操作背后的“玄学”这是原子操作中最复杂、也最容易让人困惑的部分。std::memory_order指定了原子操作周围的内存访问顺序。它解决的不是“原子性”问题而是“可见性”和“顺序性”问题。为什么需要它因为现代CPU和编译器为了性能会对指令进行重排序。在单线程环境下这种重排序不会影响最终结果。但在多线程环境下一个线程的写入操作可能不会立即被其他线程看到或者不同线程看到的操作顺序可能不一致这就导致了诡异的问题。C11定义了6种内存序从弱到强大致可分为宽松顺序只保证原子操作本身的原子性不保证操作前后的其他内存访问的顺序。性能最好用于单纯的计数器场景。获取-释放顺序一对操作loadwithmemory_order_acquire和storewithmemory_order_release能建立线程间的同步关系。一个线程的“释放”操作之前的所有写操作对另一个执行了“获取”操作的线程是可见的。这是实现锁、信号量等同步原语的基石。顺序一致顺序最强的顺序保证所有线程看到的原子操作顺序是一致的且所有非原子操作也不能跨越原子操作进行重排。性能开销最大但最符合直觉。atomic的默认操作如counter就是顺序一致的。看一个经典的“存储-加载”例子#include atomic #include thread #include iostream std::atomicint x{0}, y{0}; int r1, r2; void thread1() { x.store(1, std::memory_order_relaxed); // A r1 y.load(std::memory_order_relaxed); // B } void thread2() { y.store(1, std::memory_order_relaxed); // C r2 x.load(std::memory_order_relaxed); // D } int main() { int count 0; for (int i 0; i 100000; i) { x 0; y 0; r1 0; r2 0; std::thread t1(thread1); std::thread t2(thread2); t1.join(); t2.join(); // 在顺序一致模型中r1和r2不可能同时为0。 // 但在宽松顺序下由于指令重排A可能在B后执行C可能在D后执行导致都看到初始值0。 if (r1 0 r2 0) { count; } } std::cout r1 0 r2 0 happened count times. std::endl; return 0; }使用memory_order_relaxed时你可能会观察到r1和r2同时为0的情况这在顺序一致模型中是不可想象的。经验之谈除非你正在编写无锁数据结构或者对性能有极致的追求并且深刻理解内存模型否则请使用默认的内存序顺序一致。对于fetch_add,store,load等操作不指定内存序参数就是最强的memory_order_seq_cst。错误的弱内存序使用会引入极其隐蔽的并发Bug。先把程序写正确再考虑性能优化。5.3 原子操作与 volatile 的关键区别很多从C语言或早期嵌入式转过来的开发者容易混淆atomic和volatile。volatile关键字在C中的语义是禁止编译器对该变量的读写进行优化例如将变量缓存在寄存器中确保每次都从内存中读取。但它不保证操作的原子性也不提供多线程间的内存顺序保证。volatile的典型使用场景是内存映射的硬件寄存器每次读写都可能产生副作用。在信号处理函数或setjmp/longjmp中修改的全局变量。被不同线程访问但该平台能保证其读写是原子的如对齐的int在某些架构上并且你使用其他外部同步机制如屏障指令来保证顺序。但这非常不可移植。对于多线程共享变量volatile完全不够用。你必须使用std::atomic、互斥锁或其他同步机制。atomic已经包含了防止编译器进行不必要优化的语义类似volatile并且提供了原子性和内存序保证。6. 信号量的缺席与替代方案熟悉其他系统编程如POSIX、Windows API或Java的读者可能会问C标准库为什么没有信号量信号量是一个更古老的同步概念由Dijkstra提出它维护一个计数器提供waitP操作减一和signalV操作加一两种原子操作。可以用来控制同时访问某个资源的线程数量计数信号量或者实现简单的线程间信号传递二进制信号量相当于互斥锁。C标准委员会认为条件变量和互斥锁的组合足以实现信号量的所有功能并且更安全、更不容易出错。信号量本身不关联“所有权”概念任何线程都可以执行signal这容易导致设计上的混乱。而“锁”则明确关联了所有权谁加锁谁解锁逻辑更清晰。用condition_variable和mutex实现一个计数信号量并不复杂#include mutex #include condition_variable class counting_semaphore { private: std::mutex mtx; std::condition_variable cv; int count; public: counting_semaphore(int initial 0) : count(initial) {} void acquire() { std::unique_lockstd::mutex lock(mtx); cv.wait(lock, [this]{ return count 0; }); --count; } bool try_acquire() { std::lock_guardstd::mutex lock(mtx); if (count 0) { --count; return true; } return false; } void release() { std::lock_guardstd::mutex lock(mtx); count; cv.notify_one(); // 通知一个等待的线程 } };这个实现和标准库的std::counting_semaphoreC20引入在逻辑上是一致的。在C20之前我们通常就这样自己实现或者使用boost::semaphore。对于二进制信号量初始值为1其行为就类似于互斥锁但如前所述它没有所有权概念可以由不同线程进行acquire和release这通常不是好的设计。所以在C11/14/17中当你需要信号量功能时优先考虑用mutexcondition_variable自己实现。思考是否可以用mutex互斥、condition_variable等待/通知或atomic计数更直接地表达你的意图。很多时候信号量只是增加了不必要的复杂度。7. 实战中的选择如何为你的场景匹配合适的工具了解了这么多工具在实际项目中该如何选择这里有一个简单的决策流和对比表格。第一步明确你要保护或协调的是什么保护一个简单的共享变量如计数器、状态标志首选std::atomic。它性能最高能避免锁的开销。保护一个复杂的共享数据结构如链表、映射、队列必须使用std::mutex配合lock_guard/unique_lock。因为修改数据结构通常涉及多个内存位置原子操作无法保证整个操作的原子性。线程需要等待某个条件成立如任务队列非空使用std::condition_variable配合std::mutex。这是“等待-通知”模式的标配。希望简单地启动一个后台任务并获取结果使用std::async。它最简单。需要将任务打包放入自定义的任务队列如线程池使用std::packaged_task。需要在两个线程间传递一个值但产生值的时机很特殊使用std::promise和std::future。第二步考虑性能与复杂度原子操作 互斥锁 条件变量 异步操作在开销上从低到高但功能也从简单到复杂。永远优先选择更简单、更不易出错的工具。不要为了“性能”的臆想而过早使用弱内存序的原子操作或无锁编程。工具对比一览表特性std::mutex/std::lock_guardstd::condition_variablestd::atomicstd::async/std::future信号量 (C20前需自实现)核心用途互斥访问临界区线程间条件等待与通知无锁的原子变量操作异步执行与结果获取控制并发访问资源数量性能开销较高系统调用高系统调用上下文切换很低CPU原子指令中等有线程创建/管理开销取决于实现通常类似条件变量复杂度低中需注意虚假唤醒、条件谓词高内存序/ 低使用默认序低中常见陷阱死锁、忘记解锁虚假唤醒、丢失通知、条件谓词副作用错误的内存序导致可见性问题future析构可能阻塞、启动策略不明确没有所有权概念易误用适用场景保护任何非原子操作的共享数据生产者-消费者、线程池、等待特定状态计数器、标志位、简单的状态机后台计算、IO操作、GUI保持响应资源池如数据库连接池、限流一个综合案例简单的线程安全队列结合互斥锁和条件变量我们可以实现一个经典的线程安全队列它也是许多高级并发模式的基础组件。templatetypename T class threadsafe_queue { private: mutable std::mutex mut; std::queueT data_queue; std::condition_variable data_cond; public: threadsafe_queue() default; threadsafe_queue(const threadsafe_queue other) { std::lock_guardstd::mutex lock(other.mut); data_queue other.data_queue; } // 禁止赋值 threadsafe_queue operator(const threadsafe_queue) delete; void push(T new_value) { std::lock_guardstd::mutex lock(mut); data_queue.push(std::move(new_value)); data_cond.notify_one(); } // 等待并弹出 void wait_and_pop(T value) { std::unique_lockstd::mutex lock(mut); data_cond.wait(lock, [this]{ return !data_queue.empty(); }); value std::move(data_queue.front()); data_queue.pop(); } std::shared_ptrT wait_and_pop() { std::unique_lockstd::mutex lock(mut); data_cond.wait(lock, [this]{ return !data_queue.empty(); }); std::shared_ptrT res(std::make_sharedT(std::move(data_queue.front()))); data_queue.pop(); return res; } // 尝试弹出立即返回 bool try_pop(T value) { std::lock_guardstd::mutex lock(mut); if (data_queue.empty()) return false; value std::move(data_queue.front()); data_queue.pop(); return true; } bool empty() const { std::lock_guardstd::mutex lock(mut); return data_queue.empty(); } };这个队列可以在生产者-消费者模型中安全使用wait_and_pop让消费者在队列空时高效等待push和try_pop提供了非阻塞接口。