簡體   English   中英

易失性和CreateThread

[英]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來重新排序訪問。

volatile不是什么:

  • 使訪問原子化。
  • 防止編譯器使用非易失性訪問重新排序。
  • 從另一個線程中可見的一個線程進行更改。

在跨平台C ++中不應該依賴的一些非可移植行為:

  • VC ++擴展了volatile以防止與其他指令重新排序。 其他編譯器沒有,因為它會對優化產生負面影響。
  • x86使指針大小和較小變量的讀/寫對齊原子,並立即對其他線程可見。 其他架構則沒有。

大多數時候,人們真正想要的是柵欄(也稱為障礙)和原子指令,如果你有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的。 強制res1res2之間的這種排序只會阻止有效的優化,從而影響性能。

對於更復雜的問題,您必須使每個寫入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));
}

這適用於所有架構。 res1res2操作可以在編譯器認為合適時重新排序。 執行原子釋放可確保所有非原子操作都被排序完成,並且對執行原子獲取的線程可見。

volatile只是阻止編譯器做出假設(讀取:優化)訪問聲明為volatile的值。 換句話說,如果你聲明一些volatile東西,你基本上是說它可能隨時因編譯器不知道的原因改變它的值,所以每當你引用變量時它必須在那時查找值。
在這種情況下,編譯器可能決定在處理器寄存器中實際緩存已done的值,而與其他地方可能發生的更改無關 - 即線程2將其設置為true
我猜它在你的例子中起作用的原因是所有對done引用實際上done在內存中done的真實位置。 您不能指望始終如此,尤其是當您開始請求更高級別的優化時。
另外,我想指出,使用volatile關鍵字進行同步並不合適。 它可能恰好是原子的,但僅限於環境。 我建議你使用一個實際的線程同步結構,如wait conditionmutex 有關一個很好的解釋,請參見http://software.intel.com/en-us/blogs/2007/11/30/volatile-almost-useless-for-multi-threaded-programming/

你總是可以相信,如果做的是不是線程1將停止volatile

總是? 不是。但是在這種情況下, done的賦值在同一個模塊中,而while循環可能不會被優化掉。 取決於MSVC如何執行其優化。

通常,使用volatile聲明它更安全,以避免優化的不確定性。

它實際上比你想象的還要糟糕 - 一些編譯器可能會認為該循環是無操作循環無限循環 ,消除無限循環情況,並使其無論做什么都立即返回。 並且編譯器當然可以自由地在本地CPU寄存器中done ,並且永遠不會在循環中訪問其更新的值。 您必須使用適當的內存屏障或易失性標志變量(這在某些CPU架構上技術上是不夠的),或者像這樣的標志的鎖保護變量。

在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不是同步機制。 保證原子性和排序。 如果您不能保證在共享資源上執行的所有操作都是原子操作,那么您必須使用正確的鎖定

最后,我強烈建議閱讀這些文章:

  1. 易失性:多線程編程幾乎無用
  2. 應該是不穩定的獲取原子性和線程可見性語義?

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM