CON55-CPP. 使用条件变量时要保持线程安全和存活性
原文链接:
CON55-CPP. Preserve thread safety and liveness when using condition variables
在使用条件变量时,线程安全和存活性都是需要考虑的问题。线程安全要求在多线程环境下所有对象保持一致的状态 [Lea 2000]。存活性要求每个操作或函数调用都能够无中断地执行到结束,例如, 不会出现死锁。
条件变量必须在 while 循环内使用(详见 CON54-CPP. Wrap functions that can spuriously wake up in a loop )。为了保证存活性,在调用 condition_variable::wait() 成员函数之前,程序必须测试 while 循环条件。这个早期的测试检查其他线程是否早就已经满足条件谓词并发送了通知。在发送通知后调用 wait() 将导致无限阻塞。
为了保证线程安全,在从 wait() 返回后,程序必须测试 while 循环条件。当某个线程调用 wait() 时,它将被阻塞直到其条件变量被 condition_variable::notify_all() 或者 condition_variable::notify_one() 的调用唤醒。
notify_one() 成员函数在调用时可以解除阻塞在特定条件变量上的某一个线程。如果多个线程在同一个条件变量上等待,调度程序可以选择任何一个线程唤醒(假定所有线程具有相同的优先级级别)。
notify_all() 成员函数在调用时可以解除阻塞在特定条件变量上的所有线程。在调用 notify_all() 后,线程的执行顺序是不却确定的。因此, 一个无关的线程在发现条件谓词被满足后, 也可能开始执行, 即使应该保持休眠。
出于这些原因,线程必须在 wait() 函数返回后检查条件谓词。在调用 wait() 前后使用 while 循环既检查条件谓词时一个最佳的选择。
如果每个线程使用唯一的条件变量,则使用 notify_one() 是安全的。如果多个线程共享一个条件变量,则仅当满足以下条件时,使用 notify_one() 是安全的:
- 所有线程在唤醒后执行相同的操作,这意味着任何线程可以被单次调用的
notify_one()选中唤醒并恢复。 - 只有一个线程在接收信号后唤醒。
如果使用 notify_one() 不安全,则可以使用 notify_all() 函数解除阻塞在特定条件变量上的所有线程。
不合规代码示例(notify_one())
这个不合规代码示例使用了五个线程,这些线程按照它们在创建时分配的步骤级别依次执行(串行处理)。currentStep 变量保存当前执行步骤,并在相应线程完成后递增。最后,另一个线程被通知,以便可以执行下一步。每个线程都会等待,直到满足执行步骤,并且等待函数 wait() 在 while 循环内,符合 CON54-CPP. Wrap functions that can spuriously wake up in a loop.
1 |
|
在这个示例中,所有的线程共享一个条件变量。每个线程都有自己特定的条件谓词,因为每个线程在继续执行之前都需要 currentStep 具有不同的值。当条件变量被通知时,任何等待的线程都可以唤醒。下表解释了可能违反存活性的情况。如果恰好被通知的线程不是下一个步骤值的线程,那么该线程将再次等待。没有额外的通知产生,最终线程池将被耗尽。
死锁: 失序步骤
| 时间 | 线程 # (my_step) |
current_step |
动作 |
|---|---|---|---|
| 0 | 3 | 0 | 线程 3 第一次执行: 谓词为 false -> wait() |
| 1 | 2 | 0 | 线程 2 第一次执行: 谓词为 false -> wait() |
| 2 | 4 | 0 | 线程 4 第一次执行: 谓词为 false -> wait() |
| 3 | 0 | 0 | 线程 0 第一次执行: 谓词为 true -> currentStep++; notify_one() |
| 4 | 1 | 1 | 线程 1 第一次执行: 谓词为 true -> currentStep++; notify_one() |
| 5 | 3 | 2 | 线程 3 唤醒 (调度器的选择): 谓词为 false -> wait() |
| 6 | — | — | 线程 耗尽! 没有多余的线程, 还需要一个条件变量来唤醒其他线程. |
这个不合规代码示例违背了存活性.
合规解决方案 (notify_all())
这个合规的解决方案使用 notify_all() 来通知所有等待线程, 而不是一个随机线程. 只修改了不合规代码示例中的 run_step() 代码.
1 |
|
Awakening all threads guarantees the liveness property because each thread will execute its condition predicate test, and exactly one will succeed and continue execution.
唤醒所有线程确保了存活性, 因为每个线程讲执行各自的谓词条件检查, 并且只有一个线程条件成立继续执行.
合规解决方案(为每个线程唯一的条件变量使用 notify_one())
另一个合规的解决方案是为每个线程使用一个唯一的条件变量(所有条件变量都与同一个互斥量关联)。在这种情况下,notify_one() 仅唤醒等待它的线程。这个解决方案比使用 notify_all() 更高效,因为只唤醒了期望的线程。
被发出信号的线程的条件谓词必须为真;否则会出现死锁。
1 |
|
Risk Assessment
Failing to preserve the thread safety and liveness of a program when using condition variables can lead to indefinite blocking and denial of service (DoS).
| Rule | Severity | Likelihood | Remediation Cost | Priority | Level |
|---|---|---|---|---|---|
| CON55-CPP | Low | Unlikely | Medium | P2 | L3 |
Automated Detection
| Tool | Version | Checker | Description |
|---|---|---|---|
| CodeSonar | 7.3p0 | CONCURRENCY.BADFUNC.CNDSIGNAL | Use of Condition Variable Signal |
| Helix QAC | 2023.1 | C++1778, C++1779 | |
| Klocwork | 2023.1 | CERT.CONC.UNSAFE_COND_VAR | |
| Parasoft C/C++test | 2022.2 | CERT_CPP-CON55-a | Do not use the ‘notify_one()’ function when multiple threads are waiting on the same condition variable |
| PRQA QA-C++ | 4.4 | 5020 |
Related Vulnerabilities
Search for vulnerabilities resulting from the violation of this rule on the CERT website.
Related Guidelines
Bibliography
| [IEEE Std 1003.1:2013] | XSH, System Interfaces, pthread_cond_broadcast XSH, System Interfaces, pthread_cond_signal |
|---|---|
| [Lea 2000] |
Bibliography
| [IEEE Std 1003.1:2013] | XSH, System Interfaces, pthread_cond_broadcast XSH, System Interfaces, pthread_cond_signal |
|---|---|
| [Lea 2000] |
https://wiki.sei.cmu.edu/confluence/pages/viewpage.action?pageId=88046858)
本文标题:CON55-CPP. 使用条件变量时要保持线程安全和存活性
文章作者:xwnb
发布时间:2023-04-01
最后更新:2023-04-17
原始链接:https://xwnb.github.io/posts/3196588117/
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!并保留本声明。感谢您的阅读和支持!
分享