如何解决在某些情况下,带有临时 std::condition_variable 的线程会随机卡住
最近我在玩 std::thread
和 std::condition_variable
,但遇到了一些非常令人困惑的事情。
我的计划中有两个线程,一个提交任务,另一个执行任务。此外,还有两种std::condition_variable
。一个用于通知任务线程添加了新任务。另一个用于通知主线程任务已执行(类似于 std::promise
和 std::future
)。此外,只有完成旧任务才会提交新任务。
预期的代码执行顺序如下:
main thread task thread
| create thread |
| ---------------------> |
| push_back task 1 |
| ---------------------> |
| |
| notify task 1 done |
| <--------------------- |
| |
| ...... |
| |
| push_back task 5 |
| ---------------------> |
| |
| notify task 5 done |
| <-------------------- |
| |
| thread join |
| <--------------------- |
| |
v v
代码在这里(编辑:由于上一版本可读性差而更新了代码):
#include <cstdint>
#include <iostream>
#include <string>
#include <memory>
#include <utility>
#include <list>
#include <vector>
#include <functional>
#include <thread>
#include <future>
#include <mutex>
#include <condition_variable>
auto track(std::string const & message) -> void
{
auto static mutex = std::mutex();
std::scoped_lock _(mutex);
std::cout << message << std::endl;
}
struct task_executor
{
public:
task_executor()
: rn(true),ts(),mx(),cv(),th(&task_executor::execute,this)
{}
~task_executor()
{
{
auto ul = std::unique_lock(mx);
rn = false;
}
cv.notify_all();
th.join();
}
template<typename T> void push(T && func)
{
{
std::scoped_lock _(mx);
ts.push_back(std::forward<T>(func));
}
cv.notify_one();
}
private:
void execute()
{
auto todo = std::list<std::function<void()>>();
do {
todo.clear();
{
auto ul = std::unique_lock(mx);
cv.wait(ul,[&] { return !ts.empty() || !rn; });
std::swap(todo,ts);
}
for (auto& f : todo)
f();
} while (!todo.empty());
}
private:
bool rn;
std::list<std::function<void()>> ts;
std::mutex mx;
std::condition_variable cv;
std::thread th;
};
auto main() -> int
{
auto k = 1000;
while (k --> 0)
{
track("CASE BEGIN " + std::to_string(k));
auto te = task_executor();
for (auto i = 0; i < 6; ++i)
{
auto x = false;
auto m = std::mutex();
auto c = std::condition_variable();
track("step " + std::to_string(i) + " (1/3)");
te.push([&]
{
track("step inner (1/3)");
{
std::scoped_lock _(m);
x = true;
}
track("step inner (2/3)");
c.notify_one();
track("step inner (3/3)");
});
track("step " + std::to_string(i) + " (2/3)");
{
auto l = std::unique_lock(m);
c.wait(l,[&] { return x; });
}
track("step " + std::to_string(i) + " (3/3)");
}
track("CASE END " + std::to_string(k));
}
return 0;
}
注意事项:
- 函数
track
用于打印执行顺序,可以忽略。 - 循环 1000 次,因为单次运行并不总是会重现此问题。
- 使用
-std=c++17 -pthread
编译。
但在某些情况下,程序可能不会以这种方式执行而卡住。我已经在三种不同的操作系统和编译器下对其进行了测试:
- MSVC v142 (Visual Studio 2019) \ Windows 10 20H2
- GCC 10.2.0 \ Arch Linux(但忘记了版本)
- Clang 10.0.1 (clang-1001.0.46.4) \ Mac OS 10.14.6
它始终适用于 MSVC v142 和 GCC 10.2.0,但如果使用 Clang 10.0.1 会随机卡住。我们也可能在 online compiler 上复制它。
这是我的问题:
- 我的代码是否符合我的期望?是否有任何错误?
- 如果代码没问题,那么是 Clang 还是系统库有问题?
- 如果没有,MSVC 和 GCC 如何正确处理这种情况?
编辑:由于上一个版本不可读,我更新了代码。
解决方法
我当然可以看到潜在的 UB。
这可能会锁定线程。
考虑这种特殊的调用顺序。
时光倒流。
Main Thread: Side Thread:
Create Side Thread
cv.wait()
x = false;
Push Functions "ts"
cv.notify();
swap(ts,todo)
execute all functions in "todo"
lock(m)
x = true
unlock(m)
lock(m)
c.wait() // does not wait as x is true
unlock(m)
// leaves scope
// destructor called for
// x/m/c
c.notify(); // Calling notify
// on destroyed object
// This is UB
,
struct gate{
void open(){
auto l = std::unique_lock(m);
is_open = true;
cv.notify_all();
}
void wait()const{
auto l = std::unique_lock(m);
cv.wait(l,[&]{return is_open;});
}
private:
bool is_open = false;
mutable std::mutex m;
std::condition_variable cv;
};
然后:
for (auto i = 0; i < 6; ++i)
{
gate g;
track("step " + std::to_string(i) + " (1/3)");
te.push([&]
{
track("step inner (1/2)");
gate.open();
track("step inner (2/2)");
});
track("step " + std::to_string(i) + " (2/3)");
g.wait();
track("step " + std::to_string(i) + " (3/3)");
正如@martin 所指出的,问题看起来像是您的条件变量 c 的生命周期没有得到适当的保护,而是在它死后使用。
这会将通知移动到互斥锁中;现代 C++ 编译器也优化了这一点。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。