编译器假定静态变量不会被另一个线程修改是否合法?
Is it legal for the compiler to assume that a static variable will not be modified by another thread?
我有点惊讶编译器 (gcc) 只是假设一个静态变量永远不会被其他线程触及,即使是最低的优化级别。我试图读取从另一个线程写入的值,但 gcc 只是认为该值从未改变过。读取由另一个线程修改的静态变量的值是否符合标准未定义的行为?
我特别询问编译器所做的假设。与程序未正确处理线程同步时发生的情况无关。
为以后的读者澄清,只有所选答案清楚地回答了我在标题中所写的问题。它没有解决我遇到的实际问题,但这就是我所问的。尽管如此,我还是想澄清一下实际问题是什么,以及我是如何最终理解编译器在做什么的。
给定一个静态全局变量n
,
static int n;
我将 n
放入一个循环中以生成错误的自旋锁。
while (!n); doSth();
除非 n
是 volatile
或 _Atomic
,编译器将简单地假设 n
的值不会在循环内改变。
然后我注意到依赖于信号处理程序的代码部分按预期工作。
n = 0; //added for explanation
sigset_t s;
sigemptyset(&s);
sigaddset(&s, SIGUSR1);
sigwait(&s, (int *)&_);
if (n) doSth(); //the compiler still checks the value of `n`
我最初以为 sigwait
发生了一些特别的事情,但事实并非如此。有了这个更简单的例子,
n = 0;
putchar(0);
if (n) doSth();
编译器仍然不能假定 n
的值是 0
,因为 putchar
可能会对修改 n
的值产生副作用,因为 n
是一个全局变量。
当然,任何理智的编译器都会优化它。
n = 0;
if (n) doSth();
毕竟,有了一个好的信号处理程序,一切都运行良好。
注意:此答案指的是问题的revision 3。同时,题目也做了改动,所以这个答案不再直接对应题目。
根据 §5.1.2.4 ¶25 and ¶4 of the ISO C11 standard, two different threads accessing the same memory location using non-atomic operations in an unordered fashion causes undefined behavior,如果至少有一个线程正在写入该内存位置。
因此,编译器假定没有其他线程会更改非原子非易失性变量是合法的,除非线程以某种方式同步。
如果使用线程同步(例如互斥锁),则不再允许编译器假定变量未被另一个线程修改,除非使用 memory order 允许编译器继续做这个假设。
在您的问题中,您声明您正在尝试使用“信号”对线程进行排序。但是,在 ISO C 中,“信号”不能用于线程同步。根据 §7.14.1.1 ¶7 of the ISO C11 standard,在多线程程序中使用函数 signal
会导致未定义的行为。
如果您的意思是使用函数 cnd_signal
向条件变量发送信号,那么是的,条件变量(也使用互斥锁)可用于正确的线程同步。
如果您指的是特定于平台的功能,那么我无法对此发表评论,因为您没有在问题中指定任何特定平台。
对于那些不阅读和 DV 的人。此答案与 IPC 无关,仅回答提出的第一个问题。 IPC 对于简短的 SO 回答来说过于宽泛和复杂。我不写竞争条件、原子性或一致性。
I'm a bit surprised that the compiler (gcc) is simply assuming that a
static variable will never be touched by other threads even with the
lowest optimization level.
5.1.2.4.4 in the standard reads "Two expression evaluations conflict if one of them modifies a memory location and the other one reads or
modifies the same memory location."
你问了两个截然不同的问题。第一个是关于副作用。第二个关于IPC机制。
我只回答第一个,因为第二个太宽泛,无法在这里回答。
编译器假定只有当更改对象(变量)的代码位于正常的程序执行路径中时才能更改对象(变量)。
如果不是,则假定不会更改这些对象。
但是C有一个特殊的关键字volatile
。它通知编译器 volatile
对象容易产生副作用——即它可以被正常程序执行路径之外的东西改变。编译器每次使用都会生成read form对象存储位置,每次修改时write对象存储位置。
示例:
unsigned counter1;
volatile unsigned counter2;
int interruptHandler1(void)
{
counter1++;
}
void foo(void)
{
while(1)
if(counter1 > 100) printf("Larger!!!!");
}
int interruptHandler2(void)
{
counter2++;
}
void bar(void)
{
while(1)
if(counter2 > 100) printf("Larger!!!!");
}
输出代码:
interruptHandler1:
add DWORD PTR counter1[rip], 1
ret
.LC0:
.string "Larger!!!!"
foo:
cmp DWORD PTR counter1[rip], 100
ja .L12
.L11:
jmp .L11
.L12:
push rax
.L4:
xor eax, eax
mov edi, OFFSET FLAT:.LC0
call printf
cmp DWORD PTR counter1[rip], 100
ja .L4
.L8:
jmp .L8
interruptHandler2:
mov eax, DWORD PTR counter2[rip]
add eax, 1
mov DWORD PTR counter2[rip], eax
ret
bar:
.L20:
mov eax, DWORD PTR counter2[rip]
cmp eax, 100
jbe .L20
sub rsp, 8
.L19:
mov edi, OFFSET FLAT:.LC0
xor eax, eax
call printf
.L15:
mov eax, DWORD PTR counter2[rip]
cmp eax, 100
jbe .L15
jmp .L19
counter2:
.zero 4
counter1:
.zero 4
volatile
对象将在 任何 访问永久存储位置读取:
int foo1(void)
{
return counter1 + counter1 + counter1 + counter1;
}
int bar1(void)
{
return counter2 + counter2 + counter2 + counter2;
}
foo1:
mov eax, DWORD PTR counter1[rip]
sal eax, 2
ret
bar1:
mov eax, DWORD PTR counter2[rip]
mov esi, DWORD PTR counter2[rip]
mov ecx, DWORD PTR counter2[rip]
mov edx, DWORD PTR counter2[rip]
add eax, esi
add eax, ecx
add eax, edx
ret
并在每次修改时保存:
void foo2(void)
{
counter1++;
counter1++;
counter1++;
counter1++;
}
void bar2(void)
{
counter2++;
counter2++;
counter2++;
counter2++;
}
foo2:
add DWORD PTR counter1[rip], 4
ret
bar2:
mov eax, DWORD PTR counter2[rip]
add eax, 1
mov DWORD PTR counter2[rip], eax
mov eax, DWORD PTR counter2[rip]
add eax, 1
mov DWORD PTR counter2[rip], eax
mov eax, DWORD PTR counter2[rip]
add eax, 1
mov DWORD PTR counter2[rip], eax
mov eax, DWORD PTR counter2[rip]
add eax, 1
mov DWORD PTR counter2[rip], eax
ret
That only applies to data race conditions when "neither happens before the other". What if the value is read clearly after a modification from another thread?
“发生在之前”是一个有点棘手的概念。如果语言标准说“A 发生在 B 之前”,这并不意味着 A 总是保证实时发生在 B 之前。只有当我们把它理解为一个transitive relationship时,它的意义才变得清晰:如果按照标准,A“发生在”B之前,B“发生在”C之前;那么我们可以推断 A “发生在” C 之前。
但是,A 实际上 实时发生在 C 之前吗?
让我们想象一下有两个线程。其中之一更新了一个受互斥锁保护的共享变量:
void writer(...) {
mytype_t new_value = create_new_value(...);
pthread_mutex_lock(&mutex);
global_var = new_value;
pthread_mutex_unlock(&mutex);
另一个线程访问同一个变量:
void reader(...) {
mytype_t local_copy;
pthread_mutex_lock(&mutex);
local_copy = global_var;
pthread_mutex_unlock(&mutex);
do_something_with(local_copy);
用户 17732522 在评论中提到的一个“发生在之前”的规则是,在任何单个线程中,一切都“发生”在 程序顺序中。 也就是说,因为 global_var = new_value;
在 writer(...)
函数的源代码中出现在 pthread_mutex_unlock(&mutex);
之前,所以赋值必须在对 writer(...)
.[=23= 的任何一次调用中“发生在”解锁之前]
另一条规则说,解锁一个线程中的互斥量“发生在”其他线程锁定同一个互斥量之前。
根据这些规则,我们可以推断 *IF* 一些线程 A 调用 writer(...)
并在其他线程 B 进入 reader(...)
之前锁定互斥量,然后当线程 B 最终获取互斥量并读取 global_var
时,它将读取线程 A 写入的值。
但那是一个很大的“*IF*!” 我在这个例子中展示的任何东西实际上都不能保证线程 A 实际上 将 在线程 B 调用 reader()
之前调用 writer()
。如果您想确保线程确实以任何特定的实时顺序调用这些函数,则必须添加一些更高级别的线程间通信。
我有点惊讶编译器 (gcc) 只是假设一个静态变量永远不会被其他线程触及,即使是最低的优化级别。我试图读取从另一个线程写入的值,但 gcc 只是认为该值从未改变过。读取由另一个线程修改的静态变量的值是否符合标准未定义的行为?
我特别询问编译器所做的假设。与程序未正确处理线程同步时发生的情况无关。
为以后的读者澄清,只有所选答案清楚地回答了我在标题中所写的问题。它没有解决我遇到的实际问题,但这就是我所问的。尽管如此,我还是想澄清一下实际问题是什么,以及我是如何最终理解编译器在做什么的。
给定一个静态全局变量n
,
static int n;
我将 n
放入一个循环中以生成错误的自旋锁。
while (!n); doSth();
除非 n
是 volatile
或 _Atomic
,编译器将简单地假设 n
的值不会在循环内改变。
然后我注意到依赖于信号处理程序的代码部分按预期工作。
n = 0; //added for explanation
sigset_t s;
sigemptyset(&s);
sigaddset(&s, SIGUSR1);
sigwait(&s, (int *)&_);
if (n) doSth(); //the compiler still checks the value of `n`
我最初以为 sigwait
发生了一些特别的事情,但事实并非如此。有了这个更简单的例子,
n = 0;
putchar(0);
if (n) doSth();
编译器仍然不能假定 n
的值是 0
,因为 putchar
可能会对修改 n
的值产生副作用,因为 n
是一个全局变量。
当然,任何理智的编译器都会优化它。
n = 0;
if (n) doSth();
毕竟,有了一个好的信号处理程序,一切都运行良好。
注意:此答案指的是问题的revision 3。同时,题目也做了改动,所以这个答案不再直接对应题目。
根据 §5.1.2.4 ¶25 and ¶4 of the ISO C11 standard, two different threads accessing the same memory location using non-atomic operations in an unordered fashion causes undefined behavior,如果至少有一个线程正在写入该内存位置。
因此,编译器假定没有其他线程会更改非原子非易失性变量是合法的,除非线程以某种方式同步。
如果使用线程同步(例如互斥锁),则不再允许编译器假定变量未被另一个线程修改,除非使用 memory order 允许编译器继续做这个假设。
在您的问题中,您声明您正在尝试使用“信号”对线程进行排序。但是,在 ISO C 中,“信号”不能用于线程同步。根据 §7.14.1.1 ¶7 of the ISO C11 standard,在多线程程序中使用函数 signal
会导致未定义的行为。
如果您的意思是使用函数 cnd_signal
向条件变量发送信号,那么是的,条件变量(也使用互斥锁)可用于正确的线程同步。
如果您指的是特定于平台的功能,那么我无法对此发表评论,因为您没有在问题中指定任何特定平台。
对于那些不阅读和 DV 的人。此答案与 IPC 无关,仅回答提出的第一个问题。 IPC 对于简短的 SO 回答来说过于宽泛和复杂。我不写竞争条件、原子性或一致性。
I'm a bit surprised that the compiler (gcc) is simply assuming that a static variable will never be touched by other threads even with the lowest optimization level.
5.1.2.4.4 in the standard reads "Two expression evaluations conflict if one of them modifies a memory location and the other one reads or modifies the same memory location."
你问了两个截然不同的问题。第一个是关于副作用。第二个关于IPC机制。
我只回答第一个,因为第二个太宽泛,无法在这里回答。
编译器假定只有当更改对象(变量)的代码位于正常的程序执行路径中时才能更改对象(变量)。
如果不是,则假定不会更改这些对象。
但是C有一个特殊的关键字volatile
。它通知编译器 volatile
对象容易产生副作用——即它可以被正常程序执行路径之外的东西改变。编译器每次使用都会生成read form对象存储位置,每次修改时write对象存储位置。
示例:
unsigned counter1;
volatile unsigned counter2;
int interruptHandler1(void)
{
counter1++;
}
void foo(void)
{
while(1)
if(counter1 > 100) printf("Larger!!!!");
}
int interruptHandler2(void)
{
counter2++;
}
void bar(void)
{
while(1)
if(counter2 > 100) printf("Larger!!!!");
}
输出代码:
interruptHandler1:
add DWORD PTR counter1[rip], 1
ret
.LC0:
.string "Larger!!!!"
foo:
cmp DWORD PTR counter1[rip], 100
ja .L12
.L11:
jmp .L11
.L12:
push rax
.L4:
xor eax, eax
mov edi, OFFSET FLAT:.LC0
call printf
cmp DWORD PTR counter1[rip], 100
ja .L4
.L8:
jmp .L8
interruptHandler2:
mov eax, DWORD PTR counter2[rip]
add eax, 1
mov DWORD PTR counter2[rip], eax
ret
bar:
.L20:
mov eax, DWORD PTR counter2[rip]
cmp eax, 100
jbe .L20
sub rsp, 8
.L19:
mov edi, OFFSET FLAT:.LC0
xor eax, eax
call printf
.L15:
mov eax, DWORD PTR counter2[rip]
cmp eax, 100
jbe .L15
jmp .L19
counter2:
.zero 4
counter1:
.zero 4
volatile
对象将在 任何 访问永久存储位置读取:
int foo1(void)
{
return counter1 + counter1 + counter1 + counter1;
}
int bar1(void)
{
return counter2 + counter2 + counter2 + counter2;
}
foo1:
mov eax, DWORD PTR counter1[rip]
sal eax, 2
ret
bar1:
mov eax, DWORD PTR counter2[rip]
mov esi, DWORD PTR counter2[rip]
mov ecx, DWORD PTR counter2[rip]
mov edx, DWORD PTR counter2[rip]
add eax, esi
add eax, ecx
add eax, edx
ret
并在每次修改时保存:
void foo2(void)
{
counter1++;
counter1++;
counter1++;
counter1++;
}
void bar2(void)
{
counter2++;
counter2++;
counter2++;
counter2++;
}
foo2:
add DWORD PTR counter1[rip], 4
ret
bar2:
mov eax, DWORD PTR counter2[rip]
add eax, 1
mov DWORD PTR counter2[rip], eax
mov eax, DWORD PTR counter2[rip]
add eax, 1
mov DWORD PTR counter2[rip], eax
mov eax, DWORD PTR counter2[rip]
add eax, 1
mov DWORD PTR counter2[rip], eax
mov eax, DWORD PTR counter2[rip]
add eax, 1
mov DWORD PTR counter2[rip], eax
ret
That only applies to data race conditions when "neither happens before the other". What if the value is read clearly after a modification from another thread?
“发生在之前”是一个有点棘手的概念。如果语言标准说“A 发生在 B 之前”,这并不意味着 A 总是保证实时发生在 B 之前。只有当我们把它理解为一个transitive relationship时,它的意义才变得清晰:如果按照标准,A“发生在”B之前,B“发生在”C之前;那么我们可以推断 A “发生在” C 之前。
但是,A 实际上 实时发生在 C 之前吗?
让我们想象一下有两个线程。其中之一更新了一个受互斥锁保护的共享变量:
void writer(...) {
mytype_t new_value = create_new_value(...);
pthread_mutex_lock(&mutex);
global_var = new_value;
pthread_mutex_unlock(&mutex);
另一个线程访问同一个变量:
void reader(...) {
mytype_t local_copy;
pthread_mutex_lock(&mutex);
local_copy = global_var;
pthread_mutex_unlock(&mutex);
do_something_with(local_copy);
用户 17732522 在评论中提到的一个“发生在之前”的规则是,在任何单个线程中,一切都“发生”在 程序顺序中。 也就是说,因为 global_var = new_value;
在 writer(...)
函数的源代码中出现在 pthread_mutex_unlock(&mutex);
之前,所以赋值必须在对 writer(...)
.[=23= 的任何一次调用中“发生在”解锁之前]
另一条规则说,解锁一个线程中的互斥量“发生在”其他线程锁定同一个互斥量之前。
根据这些规则,我们可以推断 *IF* 一些线程 A 调用 writer(...)
并在其他线程 B 进入 reader(...)
之前锁定互斥量,然后当线程 B 最终获取互斥量并读取 global_var
时,它将读取线程 A 写入的值。
但那是一个很大的“*IF*!” 我在这个例子中展示的任何东西实际上都不能保证线程 A 实际上 将 在线程 B 调用 reader()
之前调用 writer()
。如果您想确保线程确实以任何特定的实时顺序调用这些函数,则必须添加一些更高级别的线程间通信。