原文链接:

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>

std::mutex mutex;
std::condition_variable cond;

void run_step(size_t myStep) {
static size_t currentStep = 0;
std::unique_lock<std::mutex> lk(mutex);

std::cout << "Thread " << myStep << " has the lock" << std::endl;

while (currentStep != myStep) {
std::cout << "Thread " << myStep << " is sleeping..." << std::endl;
cond.wait(lk);
std::cout << "Thread " << myStep << " woke up" << std::endl;
}

// Do processing...
std::cout << "Thread " << myStep << " is processing..." << std::endl;
currentStep++;

// Signal awaiting task.
cond.notify_one();

std::cout << "Thread " << myStep << " is exiting..." << std::endl;
}

int main() {
constexpr size_t numThreads = 5;
std::thread threads[numThreads];

// Create threads.
for (size_t i = 0; i < numThreads; ++i) {
threads[i] = std::thread(run_step, i);
}

// Wait for all threads to complete.
for (size_t i = numThreads; i != 0; --i) {
threads[i - 1].join();
}
}

在这个示例中,所有的线程共享一个条件变量。每个线程都有自己特定的条件谓词,因为每个线程在继续执行之前都需要 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>

std::mutex mutex;
std::condition_variable cond;

void run_step(size_t myStep) {
static size_t currentStep = 0;
std::unique_lock<std::mutex> lk(mutex);

std::cout << "Thread " << myStep << " has the lock" << std::endl;

while (currentStep != myStep) {
std::cout << "Thread " << myStep << " is sleeping..." << std::endl;
cond.wait(lk);
std::cout << "Thread " << myStep << " woke up" << std::endl;
}

// Do processing ...
std::cout << "Thread " << myStep << " is processing..." << std::endl;
currentStep++;

// Signal ALL waiting tasks.
cond.notify_all();

std::cout << "Thread " << myStep << " is exiting..." << std::endl;
}

// ... main() unchanged ...

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>

constexpr size_t numThreads = 5;

std::mutex mutex;
std::condition_variable cond[numThreads];

void run_step(size_t myStep) {
static size_t currentStep = 0;
std::unique_lock<std::mutex> lk(mutex);

std::cout << "Thread " << myStep << " has the lock" << std::endl;

while (currentStep != myStep) {
std::cout << "Thread " << myStep << " is sleeping..." << std::endl;
cond[myStep].wait(lk);
std::cout << "Thread " << myStep << " woke up" << std::endl;
}

// Do processing ...
std::cout << "Thread " << myStep << " is processing..." << std::endl;
currentStep++;

// Signal next step thread.
if ((myStep + 1) < numThreads) {
cond[myStep + 1].notify_one();
}

std::cout << "Thread " << myStep << " is exiting..." << std::endl;
}

// ... main() unchanged ...

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

Search for vulnerabilities resulting from the violation of this rule on the CERT website.

CERT Oracle Secure Coding Standard for Java THI02-J. Notify all waiting threads rather than a single thread
SEI CERT C Coding Standard CON38-C. Preserve thread safety and liveness when using condition variables
SEI CERT C++ Coding Standard CON54-CPP. Wrap functions that can spuriously wake up in a loop

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]

img img imghttps://wiki.sei.cmu.edu/confluence/pages/viewpage.action?pageId=88046858)