[英]Volatile and CreateThread
我剛剛問了一個涉及volatile: volatile array c ++的問題
然而,我的問題引發了關於volatile
的討論。
有人聲稱在使用CreateThread()
,您不必擔心volatiles
。 另一方面,Microsoft使用CreateThread()
創建的兩個線程時給出了volatile
的示例。
我在visual c ++ express 2010中創建了以下示例,如果將done
標記為volatile
,則無關緊要
#include "targetver.h"
#include <Windows.h>
#include <stdio.h>
#include <iostream>
#include <tchar.h>
using namespace std;
bool done = false;
DWORD WINAPI thread1(LPVOID args)
{
while(!done)
{
}
cout << "Thread 1 done!\n";
return 0;
}
DWORD WINAPI thread2(LPVOID args)
{
Sleep(1000);
done = 1;
cout << "Thread 2 done!\n";
return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
DWORD thread1Id;
HANDLE hThread1;
DWORD thread2Id;
HANDLE hThread2;
hThread1 = CreateThread(NULL, 0, thread1, NULL, 0, &thread1Id);
hThread2 = CreateThread(NULL, 0, thread2, NULL, 0, &thread2Id);
Sleep(4000);
CloseHandle(hThread1);
CloseHandle(hThread2);
return 0;
}
你總是可以肯定,如果線程1將停止done
是不volatile
?
什么volatile
做:
volatile
不是什么:
在跨平台C ++中不應該依賴的一些非可移植行為:
volatile
以防止與其他指令重新排序。 其他編譯器沒有,因為它會對優化產生負面影響。 大多數時候,人們真正想要的是柵欄(也稱為障礙)和原子指令,如果你有C ++ 11編譯器,或者通過編譯器和體系結構相關的函數,它們都是可用的。
Fences確保在使用時,所有先前的讀/寫操作都將完成。 在C ++ 11中,使用std::memory_order
枚舉在各個點控制圍欄。 在VC ++中,您可以使用_ReadBarrier()
, _WriteBarrier()
和_ReadWriteBarrier()
來執行此操作。 我不確定其他編譯器。
在某些體系結構(如x86)上,fence只是一種阻止編譯器重新排序指令的方法。 在其他人身上,他們可能會發出一條指令來防止CPU本身重新排序。
以下是不當使用的示例:
int res1, res2;
volatile bool finished;
void work_thread(int a, int b)
{
res1 = a + b;
res2 = a - b;
finished = true;
}
void spinning_thread()
{
while(!finished); // spin wait for res to be set.
}
在這里, finished
設置res
之前允許將finish重新排序! 那么,volatile會阻止與其他volatile的重新排序,對吧? 讓我們嘗試使每個res
也變化:
volatile int res1, res2;
volatile bool finished;
void work_thread(int a, int b)
{
res1 = a + b;
res2 = a - b;
finished = true;
}
void spinning_thread()
{
while(!finished); // spin wait for res to be set.
}
這個簡單的例子實際上可以在x86上運行,但效率很低。 其一,這股勢力res1
前設置res2
,即使我們真的不關心這個......我們只是希望他們兩個集之前finished
的。 強制res1
和res2
之間的這種排序只會阻止有效的優化,從而影響性能。
對於更復雜的問題,您必須使每個寫入volatile
。 這會使你的代碼膨脹,非常容易出錯,並且變得很慢,因為它會阻止比你真正想要的更多的重新排序。
這是不現實的。 所以我們使用柵欄和原子。 它們允許完全優化,並且只保證在柵欄點完成內存訪問:
int res1, res2;
std::atomic<bool> finished;
void work_thread(int a, int b)
{
res1 = a + b;
res2 = a - b;
finished.store(true, std::memory_order_release);
}
void spinning_thread()
{
while(!finished.load(std::memory_order_acquire));
}
這適用於所有架構。 res1
和res2
操作可以在編譯器認為合適時重新排序。 執行原子釋放可確保所有非原子操作都被排序完成,並且對執行原子獲取的線程可見。
volatile
只是阻止編譯器做出假設(讀取:優化)訪問聲明為volatile
的值。 換句話說,如果你聲明一些volatile
東西,你基本上是說它可能隨時因編譯器不知道的原因改變它的值,所以每當你引用變量時它必須在那時查找值。
在這種情況下,編譯器可能決定在處理器寄存器中實際緩存已done
的值,而與其他地方可能發生的更改無關 - 即線程2將其設置為true
。
我猜它在你的例子中起作用的原因是所有對done
引用實際上done
在內存中done
的真實位置。 您不能指望始終如此,尤其是當您開始請求更高級別的優化時。
另外,我想指出,使用volatile
關鍵字進行同步並不合適。 它可能恰好是原子的,但僅限於環境。 我建議你使用一個實際的線程同步結構,如wait condition
或mutex
。 有關一個很好的解釋,請參見http://software.intel.com/en-us/blogs/2007/11/30/volatile-almost-useless-for-multi-threaded-programming/ 。
你總是可以相信,如果做的是不是線程1將停止
volatile
?
總是? 不是。但是在這種情況下, done
的賦值在同一個模塊中,而while
循環可能不會被優化掉。 取決於MSVC如何執行其優化。
通常,使用volatile
聲明它更安全,以避免優化的不確定性。
在linux,g ++ 4.1.2上編譯,我把你的例子等同於:
#include <pthread.h>
bool done = false;
void* thread_func(void*r) {
while(!done) {};
return NULL;
}
void* write_thread_func(void*r) {
done = true;
return NULL;
}
int main() {
pthread_t t1,t2;
pthread_create(&t1, NULL, thread_func, NULL);
pthread_create(&t2, NULL, write_thread_func, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
}
當使用-O3編譯時,編譯器緩存了該值,因此它檢查了一次,然后在第一次沒有完成時進入無限循環。
但是,然后我將程序更改為以下內容:
#include <pthread.h>
bool done = false;
pthread_mutex_t mu = PTHREAD_MUTEX_INITIALIZER;
void* thread_func(void*r) {
pthread_mutex_lock(&mu);
while(!done) {
pthread_mutex_unlock(&mu);
pthread_mutex_lock(&mu);
};
pthread_mutex_unlock(&mu);
return NULL;
}
void* write_thread_func(void*r) {
pthread_mutex_lock(&mu);
done = true;
pthread_mutex_unlock(&mu);
return NULL;
}
int main() {
pthread_t t1,t2;
pthread_create(&t1, NULL, thread_func, NULL);
pthread_create(&t2, NULL, write_thread_func, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
}
雖然這仍然是一個旋轉(它只是反復鎖定/解鎖互斥鎖),但編譯器將調用更改為始終在從pthread_mutex_unlock返回后檢查done的值,從而使其正常工作。
進一步的測試表明,調用任何外部函數似乎會導致它重新檢查變量。
volatile
IS不是同步機制。 它不保證原子性和排序。 如果您不能保證在共享資源上執行的所有操作都是原子操作,那么您必須使用正確的鎖定 !
最后,我強烈建議閱讀這些文章:
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.