使用非原子布尔值而不使用互斥体来控制跨线程的控制流是否安全?

时间:2022-06-22 01:18:34

There are a number of related questions, but this question is arguably more specific.

有许多相关的问题,但这个问题可以说更具体。

Consider the following sample program, note that the bool is just an ordinary bool.

考虑以下示例程序,请注意bool只是一个普通的bool。

When I run it, it achieves the desired effect: after the user presses Enter, hello world will cease to print.

当我运行它时,它达到了预期的效果:用户按Enter后,hello world将停止打印。

If I require only that bar2() eventually starts returning immediately after run is to false, is the logic below guaranteed to be safe by the standard?

如果我只需要bar2()最终在run运行后立即开始返回false,那么下面的逻辑是否符合标准的安全性?

#include <thread>
#include <mutex>
#include <unistd.h>


bool run = true;

void bar2() {
    // Is this check safe without a lock?
    if (!run) return;
    printf("hello world\n");
    fflush(stdout);
}
void bar() {
    while (true) {
        bar2();
        sleep(1);
    }
}

int main(){
    std::thread(bar).detach();
    getchar();
    run = false;
    // Prevent main from exiting.
    getchar();
}

3 个解决方案

#1


8  

There is no guarantee by the standard that without synchronization or ordering guarantees given by e.g. std::atomic the other thread will ever see the write to run.

标准不保证没有同步或订购保证,例如由std :: atomic其他线程将看到要运行的写入。

As a matter of fact the compiler would be perfectly fine to only check once that write is true and since the thread itself never writes to it cache the value without ever reloading it. Now practically speaking with all the c library calls going on the compiler generally cannot know that none of functions writes to run and under x86 you don't have to worry about not seeing updates from other processors to memory, so it will in practice work (and even under other architectures a context switch would probably resolve the problem).

事实上,编译器完全可以只检查一次写入是否为真,并且由于线程本身从不写入它,因此缓存该值而不重新加载它。现在几乎可以说,编译器上的所有c库调用通常都不知道没有函数写入运行,而在x86下你不必担心没有看到从其他处理器到内存的更新,所以它在实际工作中(甚至在其他架构下,上下文切换可能会解决问题)。

But if you're talking purely from the standard's point of view? No guarantees whatsoever.

但是,如果你纯粹从标准的角度谈论?没有任何保证。

#2


1  

It is certainly the case that the standard makes no guarantees of how this code will behave. The testing and setting of the run variable are not properly sequenced. As a consequence, relying on the order of setting and testing constitutes undefined behaviour.

当然,标准不能保证此代码的行为方式。运行变量的测试和设置未正确排序。因此,依赖于设置和测试的顺序构成了未定义的行为。

The question of what a "real world" compiler will actually do is less easy. There are several things a compiler could reasonably do which would cause this program to fail. It could:

“真实世界”编译器实际上会做什么的问题不那么容易。编译器可以合理地做几件事会导致这个程序失败。它可以:

  1. Detect the UB and institute its own implentation-specific course of action, which might work as you wanted, or not. At least if you test it, you will find out which.
  2. 检测UB并制定自己的特定于实施的行动方案,这可能会按您的意愿行事。至少如果你测试它,你会发现哪个。
  3. Make the assumption that UB has not occurred, and delete the if(!run) test from the program, since without the UB it can never have any effect.
  4. 假设没有发生UB,并从程序中删除if(!run)测试,因为没有UB它永远不会有任何影响。
  5. Observe that the run variable is never tested after it was set, and delete the run=false, since it can never have any effect. [This may involve assuming no UB.]
  6. 观察运行变量在设置后从未进行过测试,并删除run = false,因为它永远不会有任何影响。 [这可能涉及假设没有UB。]

If it was my choice, I wouldn't rely on this code. I would use the capabilities provided by the standard and write conforming behaviour.

如果这是我的选择,我不会依赖这个代码。我会使用标准提供的功能并编写符合规范的行为。

#3


-6  

yes, it is completely safe. most processors have 32 bit pipelines.

是的,这是完全安全的。大多数处理器都有32位流水线。

#1


8  

There is no guarantee by the standard that without synchronization or ordering guarantees given by e.g. std::atomic the other thread will ever see the write to run.

标准不保证没有同步或订购保证,例如由std :: atomic其他线程将看到要运行的写入。

As a matter of fact the compiler would be perfectly fine to only check once that write is true and since the thread itself never writes to it cache the value without ever reloading it. Now practically speaking with all the c library calls going on the compiler generally cannot know that none of functions writes to run and under x86 you don't have to worry about not seeing updates from other processors to memory, so it will in practice work (and even under other architectures a context switch would probably resolve the problem).

事实上,编译器完全可以只检查一次写入是否为真,并且由于线程本身从不写入它,因此缓存该值而不重新加载它。现在几乎可以说,编译器上的所有c库调用通常都不知道没有函数写入运行,而在x86下你不必担心没有看到从其他处理器到内存的更新,所以它在实际工作中(甚至在其他架构下,上下文切换可能会解决问题)。

But if you're talking purely from the standard's point of view? No guarantees whatsoever.

但是,如果你纯粹从标准的角度谈论?没有任何保证。

#2


1  

It is certainly the case that the standard makes no guarantees of how this code will behave. The testing and setting of the run variable are not properly sequenced. As a consequence, relying on the order of setting and testing constitutes undefined behaviour.

当然,标准不能保证此代码的行为方式。运行变量的测试和设置未正确排序。因此,依赖于设置和测试的顺序构成了未定义的行为。

The question of what a "real world" compiler will actually do is less easy. There are several things a compiler could reasonably do which would cause this program to fail. It could:

“真实世界”编译器实际上会做什么的问题不那么容易。编译器可以合理地做几件事会导致这个程序失败。它可以:

  1. Detect the UB and institute its own implentation-specific course of action, which might work as you wanted, or not. At least if you test it, you will find out which.
  2. 检测UB并制定自己的特定于实施的行动方案,这可能会按您的意愿行事。至少如果你测试它,你会发现哪个。
  3. Make the assumption that UB has not occurred, and delete the if(!run) test from the program, since without the UB it can never have any effect.
  4. 假设没有发生UB,并从程序中删除if(!run)测试,因为没有UB它永远不会有任何影响。
  5. Observe that the run variable is never tested after it was set, and delete the run=false, since it can never have any effect. [This may involve assuming no UB.]
  6. 观察运行变量在设置后从未进行过测试,并删除run = false,因为它永远不会有任何影响。 [这可能涉及假设没有UB。]

If it was my choice, I wouldn't rely on this code. I would use the capabilities provided by the standard and write conforming behaviour.

如果这是我的选择,我不会依赖这个代码。我会使用标准提供的功能并编写符合规范的行为。

#3


-6  

yes, it is completely safe. most processors have 32 bit pipelines.

是的,这是完全安全的。大多数处理器都有32位流水线。