现实生活中是否存在简单的布尔指针作为线程取消标志不会有效取消线程的情况?
Is there a real-life situation where a simple pointer-to-bool as thread cancellation flag will not effectively cancel a thread?
首先,我理解正式地,使用非 atomic
标志来取消线程是非常未定义的行为,因为语言没有指定之前是否会写入此变量线程退出。
在工作中,这是很久以前实现的,大多数计算线程在整个工作过程中都会检查这个 bool
的值,以优雅地取消它们正在做的任何事情。当我第一次看到这个时,我的第一反应是改变所有这些以使用更好的方式(在这种情况下,QThread::requestInterruption
和 QThread::interruptionRequested
似乎是一个可行的选择)。快速搜索代码,在整个代码库中发现了大约 800 次 variable/construct,所以我放弃了。
当我接触一位(资深,以多年经验而言)同事时,他向我保证,尽管这可能确实是错误的,但他从未见过它无法实现其目的。他认为,唯一会出错的情况是,如果允许(一组)线程 运行 而另一个实际更改此标志的线程永远不会被允许执行,直到其他线程完成。他还争辩说,在这种情况下,OS 会进行干预并在所有线程中公平地分配 运行 时间,从而可能导致取消延迟。
现在我的问题是:在现实生活中是否存在确实失败的情况(最好是在常规系统上,基于 x86/ARM,最好是 C 或 C++)?
请注意,我并不是要赢得争论,因为我的同事同意这在技术上是不正确的,但我想知道它是否会导致问题以及在什么情况下可能会发生这种情况。
解决这个问题的最简单方法是将其简化为一个相当简单的示例。编译器将优化读取标志,因为它不是原子的并且被另一个线程写入是 UB;因此该标志实际上不会 read.
您同事的论点是基于这样的假设,即当您取消引用该标志时,编译器实际上会 加载 该标志。但实际上它没有义务这样做。
#include <thread>
#include <iostream>
bool cancelled = false;
bool finished = false;
void thread1() {
while(!cancelled) {
std::cout << "Not cancelled";
}
}
int main() {
std::thread t(thread1);
t.detach();
cancelled = true;
while(!finished) {}
}
到 coliru 上的 运行,加载 http://coliru.stacked-crooked.com/a/5be139ee34bf0a80,您将需要编辑并进行一些细微的更改,因为对于不终止的片段,缓存已损坏。
实际上,他只是在打赌编译器的优化器会做得很差,这似乎是一件非常糟糕的事情。
从 Mine 的评论可以推断,Puppy 的代码示例没有演示问题。需要进行一些小的修改。
首先,我们必须在thread1
的末尾加上finished = true;
,这样程序甚至可以假装可以终止。
现在,优化器无法检查每个翻译单元中的每个函数以确保 cancelled
在输入 thread1
时实际上总是 false
,因此它不能进行大胆的优化以删除 while 循环及其后的所有内容。我们可以通过在 thread1
.
的开头将 cancelled
设置为 false 来解决这个问题
加上前面的,为了公平起见,我们还要在main
中不断的设置cancelled
为true,否则无法保证main
中的单次赋值不被调度在 thread1
.
的初始赋值之后
编辑:添加了限定符和同步连接而不是分离。
#include <thread>
#include <iostream>
bool cancelled = false;
bool finished = false;
void thread1() {
cancelled = false;
while(!cancelled)
;
finished = true;
}
int main() {
std::thread t(thread1);
while(!finished) {
std::cout << "trying to cancel\n";
cancelled = true;
}
t.join();
}
只要您在使用数据之前等待线程完成,您在实践中就没问题:std::thread::join
或 QThread::wait
设置的内存屏障会保护您。
您担心的不是 cancelled
变量,只要它是易变的,您就可以正常使用。您应该担心读取线程修改的数据的不一致状态。
首先,我理解正式地,使用非 atomic
标志来取消线程是非常未定义的行为,因为语言没有指定之前是否会写入此变量线程退出。
在工作中,这是很久以前实现的,大多数计算线程在整个工作过程中都会检查这个 bool
的值,以优雅地取消它们正在做的任何事情。当我第一次看到这个时,我的第一反应是改变所有这些以使用更好的方式(在这种情况下,QThread::requestInterruption
和 QThread::interruptionRequested
似乎是一个可行的选择)。快速搜索代码,在整个代码库中发现了大约 800 次 variable/construct,所以我放弃了。
当我接触一位(资深,以多年经验而言)同事时,他向我保证,尽管这可能确实是错误的,但他从未见过它无法实现其目的。他认为,唯一会出错的情况是,如果允许(一组)线程 运行 而另一个实际更改此标志的线程永远不会被允许执行,直到其他线程完成。他还争辩说,在这种情况下,OS 会进行干预并在所有线程中公平地分配 运行 时间,从而可能导致取消延迟。
现在我的问题是:在现实生活中是否存在确实失败的情况(最好是在常规系统上,基于 x86/ARM,最好是 C 或 C++)?
请注意,我并不是要赢得争论,因为我的同事同意这在技术上是不正确的,但我想知道它是否会导致问题以及在什么情况下可能会发生这种情况。
解决这个问题的最简单方法是将其简化为一个相当简单的示例。编译器将优化读取标志,因为它不是原子的并且被另一个线程写入是 UB;因此该标志实际上不会 read.
您同事的论点是基于这样的假设,即当您取消引用该标志时,编译器实际上会 加载 该标志。但实际上它没有义务这样做。
#include <thread>
#include <iostream>
bool cancelled = false;
bool finished = false;
void thread1() {
while(!cancelled) {
std::cout << "Not cancelled";
}
}
int main() {
std::thread t(thread1);
t.detach();
cancelled = true;
while(!finished) {}
}
到 coliru 上的 运行,加载 http://coliru.stacked-crooked.com/a/5be139ee34bf0a80,您将需要编辑并进行一些细微的更改,因为对于不终止的片段,缓存已损坏。
实际上,他只是在打赌编译器的优化器会做得很差,这似乎是一件非常糟糕的事情。
从 Mine 的评论可以推断,Puppy 的代码示例没有演示问题。需要进行一些小的修改。
首先,我们必须在thread1
的末尾加上finished = true;
,这样程序甚至可以假装可以终止。
现在,优化器无法检查每个翻译单元中的每个函数以确保 cancelled
在输入 thread1
时实际上总是 false
,因此它不能进行大胆的优化以删除 while 循环及其后的所有内容。我们可以通过在 thread1
.
cancelled
设置为 false 来解决这个问题
加上前面的,为了公平起见,我们还要在main
中不断的设置cancelled
为true,否则无法保证main
中的单次赋值不被调度在 thread1
.
编辑:添加了限定符和同步连接而不是分离。
#include <thread>
#include <iostream>
bool cancelled = false;
bool finished = false;
void thread1() {
cancelled = false;
while(!cancelled)
;
finished = true;
}
int main() {
std::thread t(thread1);
while(!finished) {
std::cout << "trying to cancel\n";
cancelled = true;
}
t.join();
}
只要您在使用数据之前等待线程完成,您在实践中就没问题:std::thread::join
或 QThread::wait
设置的内存屏障会保护您。
您担心的不是 cancelled
变量,只要它是易变的,您就可以正常使用。您应该担心读取线程修改的数据的不一致状态。