我有點驚訝編譯器 (gcc) 只是假設靜態變數永遠不會被其他執行緒觸及,即使優化級別最低。我試圖讀取從另一個執行緒寫入的值,但 gcc 只是認為該值從未改變。是否按照標準讀取由另一個執行緒未定義行為修改的靜態變數的值?
我特別詢問編譯器所做的假設。不是關于當程式沒有正確處理執行緒同步時會發生什么。
為了向未來的讀者澄清,只有選定的答案才能清楚地回答標題中所寫的我的問題。它沒有解決我遇到的實際問題,但這就是我要問的。不過,我想澄清一下實際問題是什么,以及我是如何最終理解編譯器在做什么的。
給定一個靜態全域變數n,
static int n;
我放入n一個回圈以制作一個錯誤的自旋鎖。
while (!n); doSth();
除非n是volatileor _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();
編譯器仍然無法假設nis的值,0因為putchar修改n因為n是全域變數的值可能會產生副作用。
當然,任何健全的編譯器都會對此進行優化。
n = 0;
if (n) doSth();
畢竟,有了一個不錯的信號處理程式,一切都很好。
uj5u.com熱心網友回復:
注意:此答案指的是問題的修訂版 3。同時,問題已更改,因此此答案不再直接對應于問題。
根據ISO C11 標準的 §5.1.2.4 ?25 和 ?4,如果至少有一個執行緒正在寫入該記憶體位置,則兩個不同的執行緒以無序方式使用非原子操作訪問同一記憶體位置會導致未定義行為。
因此,編譯器假定沒有其他執行緒會更改非原子非易失性變數是合法的,除非執行緒以某種方式同步。
如果使用執行緒同步(例如互斥鎖),則不再允許編譯器假設變數沒有被另一個執行緒修改,除非使用了允許編譯器繼續進行此假設的記憶體順序。
在您的問題中,您宣告您正在嘗試使用“信號”來訂購執行緒。但是,在 ISO C 中,“信號”不能用于執行緒同步。根據ISO C11 標準的 §7.14.1.1 ?7,signal在多執行緒程式中使用該函式會導致未定義的行為。
如果您的意思是使用 function 向條件變數發送信號cnd_signal,那么是的,條件變數(也使用互斥鎖)可用于正確的執行緒同步。
如果您指的是特定于平臺的功能,那么我無法對此發表評論,因為您沒有在問題中指定任何特定平臺。
uj5u.com熱心網友回復:
對于那些不閱讀和 DV 的人。此答案不相關 IPC 僅回答提出的第一個問題。IPC 對于簡短的 SO 答案來說過于寬泛和復雜。我不寫競爭條件、原子性或一致性。
我有點驚訝編譯器 (gcc) 只是假設靜態變數永遠不會被其他執行緒觸及,即使優化級別最低。
標準中的 5.1.2.4.4 讀取“如果其中一個修改記憶體位置而另一個讀取或修改相同的記憶體位置,則兩個運算式評估會發生沖突。”
你問了兩個截然不同的問題。第一個是關于副作用。第二個關于IPC機制。
我將只回答第一個,因為第二個太寬泛而無法在 SO 上回答。
編譯器假設物件(變數)只有在更改它們的代碼在正常程式執行路徑中時才能更改。
如果不是,則假定不會更改這些物件。
但是C 有一個特殊的關鍵字volatile。它通知編譯器volatile物件容易產生副作用——即它可以被正常程式執行路徑之外的東西改變。編譯器每次使用都會從物件存盤位置生成讀取,每次修改時寫入物件存盤位置。
例子:
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
uj5u.com熱心網友回復:
這僅適用于“兩者都不在另一個之前發生”時的資料競爭條件。如果從另一個執行緒修改后可以清楚地讀取值怎么辦?
“之前發生”是一個有點棘手的概念。如果語言標準說“A 在 B 之前發生”,這并不意味著 A 總是實時地保證在 B 之前發生。只有當我們把它理解為一種傳遞關系時,它的含義才變得清晰:如果按照標準,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(...)。
另一條規則是在一個執行緒中解鎖互斥鎖“發生在”其他執行緒鎖定同一個互斥鎖之前。
從這些規則我們可以推斷*IF*某個執行緒Awriter(...)在其他執行緒B進入之前呼叫并鎖定了互斥鎖reader(...),那么當執行緒B最終獲取互斥鎖并讀取 時global_var,它將讀取執行緒A寫入的值。
但是,這是一個很大的“ * IF *! ”沒有,我在這個例子表明實際上保證執行緒A實際上將呼叫writer()之前執行緒B呼叫reader()。如果您想確保執行緒確實以任何特定的實時順序呼叫這些函式,則必須添加一些更高級別的執行緒間通信。
轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/399905.html
