Skip to main content

Concurrency系列(四): 與Synchronizes-With同在

在談論Concurrency時,常常會看到許多文件、文章使用Synchronized-with這個詞彙,但是深入google你會發現,網路上關於這個詞的資訊並不多,C++官方文件也沒有提出很明確的定義或解釋,但是他們依舊繼續使用這個詞,只說明有一些操作可以建立synchronizes-with的關係,這個詞大家並沒有給出一個很明確的定義。

我先用自己的話向大家解釋什麼是synchronized-with

synchronized-with是個發生在兩個不同thread間的同步行為,當A synchronized-with B的時,代表A對記憶體操作的效果,對於B是可見的。而A和B是兩個不同的thread的某個操作。

你會發現,其實synchronized-with就是跨thread版本的happens-before。

從Java切入

當在一個multithread的環境下,你要如何確定thread A執行someVariable = 3,那麼其他thread能夠看到3真的被寫入someVariable?

實際上,有很多原因會讓其他thread不會立刻看到someVariable為3,可能是compiler做instruction reorder,把指令重排讓程式更有效率,也可能是someVariable還在register內、或是被處理器寫到cache,但還沒被寫到到main memory上,甚至是其他thread嘗試讀取someVariable的時後讀到舊的cache資料。

因此,Java必須要定義一些特殊的語法,像是volatile, synchronized, final來確保針對同一個變數的跨thread記憶體操作能夠正確的同步。

synchronized keyword

public class SynchronizedCounter {
private int c = 0;
public synchronized void increment() {
c++;
}
public synchronized void decrement() {
c--;
}
public synchronized int value() {
return c;
}
}

可以看到上面的程式碼中,每個方法前面都加了一個關鍵字synchronized,這個關鍵字有兩個效果

  • Mutual Exclusive
    • 對同一個物件而言,不可能有兩個前綴synchronized的方法同時交錯執行,當一個thread正在執行前綴synchronized的方法時,其他想執行synchronized方法的thread會被擋住。
  • 建立Happens Before關係
    • 對同一個物件而言,當一個thread離開synchronized方法時,會自動對接下來呼叫synchronized方法的thread建立一個happens-before關係,前一個synchronized的方法對該物件所做的修改,保證對接下來進入synchronized方法的thread可見。

要確保這件事情,代表JVM必須要做兩件事,一個是在離開synchronized區段時,把local processor的cache寫入到記憶體內,另一個是在進入下一個synchronized前,要讓local cache失效,使處理器重新去main memory抓正確的值。這樣才能夠確保每次進入synchronized區段時,物件的狀態是最新的。

Volatile keyword

另一個常見的用法是Java的volatile,如果你將物件內的變數宣告為volatile,那麼不同thread對該變數的讀寫,保證是atomic的,而且會讀到最新寫入的值。如果我用正式一點的術語描述,就是 A write to a volatile field happens-before every subsequent read of that same volatile

thread create/join

Java在開新thread時,也會建立起跨thread的happens-before關係(其實就是synchronized-with)。當thread A呼叫Thread.start建立thread B時,thread A呼叫start之前對記憶體產生的影響對於thread B可見。 當thread A要結束時,thread B呼叫Thread.join等待thread A結束,此時也會建立起跨thread的happens-before關係,thread A結束前對記憶體的影響對於呼叫join之後的thread B可見。

圖解

img

這樣我們就能理解上圖了,左邊thread A確保每一行都happens-before下一行,右邊的thread B也確保每一行都happens-before下一行,因此如果我對unlock M和lock M建立了synchronized-with(跨thread的happens-before)的關係,那麼所有unlock M之前的效果,對於lock M之後都可見。

再來輪到C++

C++比Java討厭的地方在於,Java努力把各種底層的複雜性藏起來,讓上層的JVM提供一個一致的環境,得以達成Write Once, Run Anywhere的理想。但C++盡可能提供你所有你能做到的事情,即使你可能會誤用這些工具。

在2014年C++的官方標準文件(Standard for Programming Language C++)N4296的第12頁,提示了C++提供的同步操作,也就是使用atomic operation或是mutex:

The library defines a number of atomic operations and operations on mutexes that are specially identified as synchronization operations. These operations play a special role in making assignments in one thread visible to another.

如上所述,C++定義了一系列的atomic operation和mutex,來協助你建立跨thread間的同步關係。實際上還有更多語法可用,但我們來看一個簡單的mutex例子:

#include<iostream> // std::cout
#include<thread> // std::thread
#include<mutex> // std::mutex

using std::mutex mtx; // mutex for critical section
int count = 0;
void printthreadid (int id) {
// critical section (exclusive access to std::cout signaled by locking mtx):
mtx.lock();
std::cout << "thread #" << id << " count:" << count << '\n';
count++;
mtx.unlock();
}

int main ()
{
std::thread threads[10];
// spawn 10 threads:
for (int i=0; i < 10; ++i)
threads[i] = std::thread(printthreadid,i+1);
for (auto& th : threads) th.join();
return 0;
}

上述的程式碼一次開10個thread,每個thread做的事情都一樣,印出傳入的參數和counter目前的值,之後把counter++。要是沒有加上mutex lock,因為thread間交錯執行,無法確保synchronized-with的關係,上個thread執行的效果無法保證傳遞給下一個thread,於是印的亂七八糟,counter數字也亂跳。 沒有加上mutex lock後果如下

thread #thread #thread #thread #12 count: count:00
3 count:0
thread #5 count:0
thread #4 count:0
6 count:2
thread #7 count:6
thread #8 count:7
thread #9 count:8
thread #10 count:9

加上lock之後,結果就正常多了,每個thread都正確地把內容印出來

thread #2 count:0
thread #1 count:1
thread #8 count:2
thread #4 count:3
thread #5 count:4
thread #6 count:5
thread #9 count:6
thread #7 count:7
thread #3 count:8
thread #10 count:9

再次回到Synchronizes-with

實際上,要建立synchornizes-with的關係有很多種不同層次的方法,Jeff Preshing介紹Synchorinzes-with的文章內提供了下面這張詳盡的圖,這張圖只是個大致的示意,實際上可能還有其他方法可以建立同步關係。

可以看到synchronizes-with是一種跨thread間的happens-before關係,此外我們可以透過mutex lock, thread create/join、Aquire and release Semantic來建立synchronized-with關係,因為Aquire and Release是另外一個較為複雜的概念,所以我打算另外開一篇文章再來談,其餘下有更底層的C++ atomic type, memory fence和volatile types等語法,之後再來介紹。

Reference