潜在的数据竞赛是数据竞赛吗?
Is a potential data race a data race?
我想了解数据竞争和无日期竞争之间的界限在哪里,以及关于未定义行为的后果是什么。
考虑这个例子:
#include <chrono>
#include <thread>
#include <cstdlib>
#include <iostream>
#include <ctime>
#include <functional>
void write(int delay, int& value, int target) {
std::this_thread::sleep_for(std::chrono::milliseconds(delay));
value = target;
}
int main() {
int x;
std::srand(std::time(nullptr));
auto t1 = std::thread(write, rand()%100, std::ref(x), 42);
auto t2 = std::thread(write, rand()%100, std::ref(x), 24);
t1.join();
t2.join();
std::cout << x;
}
这段代码是总是有数据竞争,还是只是偶尔有?根据标准,上述代码的行为是始终未定义,还是仅有时未定义(取决于 rand()
的结果)?
PS:当然,我不知道输出是 42
还是 24
,但在存在未定义行为的情况下,我什至不会期望两者中的任何一个当然,它可能是 123
或 "your cat ate my fish"
.
PPS:我不关心高质量的随机性,因此 rand()
适合这个例子。
此代码总是存在数据竞争。两次写入之间没有 happens-before 顺序,因此存在数据竞争。
OS原则上可以调度两个线程,使两个睡眠同时返回,只要每个睡眠至少与指定的延迟时间一样长。
在可以在单个总线周期中将值存储到 int
的体系结构上,您将获得 42
或 24
作为输出,永远不会 123
或任何其他值。但是,理论上你可以有一个多核处理器,其中 int
大于本机数据宽度,需要多个存储,在这种情况下,两个线程的存储可能会交错。当尝试存储到 uint64_t
.
时,这实际上发生在 32 位处理器上
许多平台可以以基本上零成本提供比标准要求更强大的有用行为保证,但不幸的是,标准没有提供程序可以确定哪些保证可用的方法,并且优化器可能会重新安排代码而不考虑是否会破坏利用平台本应以基本零成本提供的保证的程序。
例如,给定 int x, y, *p;
,请考虑以下代码片段:
x = 0x0123;
y = *p;
... maybe some computations here that use up CPU registers
x = 0x0124;
一个实现可能会注意到在 0x0123
和 0x0124
的存储之间,x
的值被读取但未被写入,因此可以更改后者从像 mov ax,0124h / mov _x,ax
或 mov word [_x],0124h
这样的序列(在 8088 上,任何一个序列都是六个字节加两个内存操作)写入 inc byte _x
(四个字节加两个内存操作)。然而,如果中间有一些其他值(如 0x00FF)的写入,则此类代码可能会出现严重故障。下游代码可能会将 x
的任何非零值视为同样可接受的,并且源代码从未要求写入任何底部字节为零的值,但如果外部写入在 [=17 之前存储 0x00FF =] 指令,x
可能会保留零,这是出乎意料的。
在许多情况下,避免进行此类优化的成本将低于包含足以防止数据竞争的内存屏障的成本,但不幸的是,除非有人替换 x
与 "atomic int",这可能需要额外的存储空间,并更改对 x
的所有访问以明确使用宽松的语义。
我想了解数据竞争和无日期竞争之间的界限在哪里,以及关于未定义行为的后果是什么。
考虑这个例子:
#include <chrono>
#include <thread>
#include <cstdlib>
#include <iostream>
#include <ctime>
#include <functional>
void write(int delay, int& value, int target) {
std::this_thread::sleep_for(std::chrono::milliseconds(delay));
value = target;
}
int main() {
int x;
std::srand(std::time(nullptr));
auto t1 = std::thread(write, rand()%100, std::ref(x), 42);
auto t2 = std::thread(write, rand()%100, std::ref(x), 24);
t1.join();
t2.join();
std::cout << x;
}
这段代码是总是有数据竞争,还是只是偶尔有?根据标准,上述代码的行为是始终未定义,还是仅有时未定义(取决于 rand()
的结果)?
PS:当然,我不知道输出是 42
还是 24
,但在存在未定义行为的情况下,我什至不会期望两者中的任何一个当然,它可能是 123
或 "your cat ate my fish"
.
PPS:我不关心高质量的随机性,因此 rand()
适合这个例子。
此代码总是存在数据竞争。两次写入之间没有 happens-before 顺序,因此存在数据竞争。
OS原则上可以调度两个线程,使两个睡眠同时返回,只要每个睡眠至少与指定的延迟时间一样长。
在可以在单个总线周期中将值存储到 int
的体系结构上,您将获得 42
或 24
作为输出,永远不会 123
或任何其他值。但是,理论上你可以有一个多核处理器,其中 int
大于本机数据宽度,需要多个存储,在这种情况下,两个线程的存储可能会交错。当尝试存储到 uint64_t
.
许多平台可以以基本上零成本提供比标准要求更强大的有用行为保证,但不幸的是,标准没有提供程序可以确定哪些保证可用的方法,并且优化器可能会重新安排代码而不考虑是否会破坏利用平台本应以基本零成本提供的保证的程序。
例如,给定 int x, y, *p;
,请考虑以下代码片段:
x = 0x0123;
y = *p;
... maybe some computations here that use up CPU registers
x = 0x0124;
一个实现可能会注意到在 0x0123
和 0x0124
的存储之间,x
的值被读取但未被写入,因此可以更改后者从像 mov ax,0124h / mov _x,ax
或 mov word [_x],0124h
这样的序列(在 8088 上,任何一个序列都是六个字节加两个内存操作)写入 inc byte _x
(四个字节加两个内存操作)。然而,如果中间有一些其他值(如 0x00FF)的写入,则此类代码可能会出现严重故障。下游代码可能会将 x
的任何非零值视为同样可接受的,并且源代码从未要求写入任何底部字节为零的值,但如果外部写入在 [=17 之前存储 0x00FF =] 指令,x
可能会保留零,这是出乎意料的。
在许多情况下,避免进行此类优化的成本将低于包含足以防止数据竞争的内存屏障的成本,但不幸的是,除非有人替换 x
与 "atomic int",这可能需要额外的存储空间,并更改对 x
的所有访问以明确使用宽松的语义。