C++20协程与std::mutex的规范、问题及解决方案问询
C++20协程与std::mutex的使用规范、问题及解决方案
一、核心规范与通用问题
- C++20标准未针对协程单独制定
std::mutex的特殊规范,std::mutex的原有规则(如非递归锁禁止同一线程重复锁定、阻塞时占用线程等)依然完全适用。 - 协程与
std::mutex结合的核心矛盾源于协程的协作式调度特性:当协程持有锁时发生阻塞或挂起,会导致线程被占用无法处理其他协程,甚至引发死锁等未定义行为。
二、示例分析与疑问解答
示例1:带std::mutex与阻塞操作的协程
task<> with_mutex_and_blocking_op() { Result result; { std::lock_guard<std::mutex> lock(some_non_recursive_mutex); // 确保同一时间仅一个协程执行call_blocking_io result = call_blocking_io(); // 此处阻塞直到IO完成 } co_await some_awaiter(result); }
- 这确实是协程中的反模式:协程设计初衷是避免阻塞线程,而
call_blocking_io()会直接阻塞当前线程,完全违背协程的异步优势。 - 你担心的场景真实存在:协程A持有锁后阻塞,调度器切换到同线程的协程B,B尝试获取同一非递归锁时,会触发同一线程重复锁定非递归mutex的未定义行为(可能直接死锁或程序崩溃)。
- 解决方案:
- 替换阻塞IO为异步IO操作,通过
co_await完成,避免线程阻塞; - 若必须使用阻塞操作,将其放到单独的线程池中执行,协程通过
co_await等待任务完成,避免占用协程调度线程; - 临时替代方案可使用
std::recursive_mutex(不推荐,会掩盖设计缺陷且性能更差)。
- 替换阻塞IO为异步IO操作,通过
示例2:带std::mutex的await_suspend
void await_suspend(std::coroutine_handle<> handle) { auto task = create_an_async_task(); std::lock_guard<std::mutex> lock(some_non_recursive_mutex); task_queue.push(std::move(task)); }
- 问题分析:
await_suspend在协程挂起时执行,若std::lock_guard获取锁时阻塞,当前线程会被卡住,调度器无法切换到其他协程(线程被mutex的阻塞调用占用)。这种情况不会出现示例1的同线程重复锁问题,但会导致线程资源浪费——协程调度线程被锁阻塞,无法处理其他协程任务。 - 解决方案:
- 使用非阻塞锁操作,比如
std::mutex::try_lock(),若获取失败则重新安排协程稍后尝试,避免阻塞线程; - 改用异步锁机制(如基于条件变量的异步互斥体,或第三方库提供的异步锁),让协程在无法获取锁时挂起,而非阻塞线程。
- 使用非阻塞锁操作,比如
示例3:用std::mutex保护co_await的协程
- 核心问题:临界区中调用
co_await会导致协程挂起,此时锁仍被持有,其他协程(尤其是同线程的)尝试获取锁会触发死锁或未定义行为。 - C++20未推出专门解决此问题的新特性,当前主流解决方案依然是:
- 避免在持有锁的临界区内执行
co_await,将co_await移到临界区外; - 使用支持异步等待的互斥体(如
std::experimental::async_mutex,或Boost.Asio等库中的异步锁),这类锁允许协程在无法获取锁时挂起,且挂起时不会持有锁; - 重构代码逻辑,用其他同步机制替代mutex,比如基于任务队列的串行化执行,确保同一任务链中的操作顺序执行,无需显式锁。
- 避免在持有锁的临界区内执行
三、通用最佳实践
- 协程中优先使用异步同步原语,避免依赖
std::mutex这类阻塞式锁; - 绝对不要在持有锁的情况下执行阻塞操作或
co_await; - 若必须使用阻塞式锁,确保锁的持有时间极短,且不会在持有锁时触发协程挂起;
- 利用协程的调度特性,将阻塞任务卸载到专门的线程池,协程通过
co_await等待任务完成。
内容的提问来源于stack exchange,提问作者for_stack
相关产品推荐
相关产品推荐

