0%

volatile keyword in different languages

volatile最早出現在C語言。之後C++,Java,C#都沿用了這個關鍵字,不過語義大不相同。

C / C++

volatile一開始的用途就是用來作Memory Mapped I/O用的。看看以下程式碼

1
2
3
4
5
static int foo;
void bar(void) {
foo = 0;
while (foo != 255) ;
}

聰明的Compiler在做最佳化的時候,大概會變成這個樣子。

1
2
3
4
void bar_optimized(void) {
foo = 0;
while (true) ;
}

Compiler跟CPU只會保證執行上下文結果是正確的,foo在這邊沒有被改變,所以foo != 255永遠為true,所以就變成無窮迴圈了。如果外界改變了foo的值,整個迴圈也有可能不會結束。 因此volatile因此而生。
自從Multithread跟Multiore programming逐漸風行之後,大家都想找到一個比lok跟輕量化的解決方案。因此valatile又被考慮了。
至於這方面晚點在說,先來說Java的volatile。

Java / C Sharp

Java由於是跑在Virtual Machine上,所以所設計理念必須超越特定硬體規格。 Java Memory Model定義,一個Thread讀取一個變數時,會存在Thread的Local Spaee上運作,等到運算過後,找到是當實機再將值寫為全域空間。因此如果兩個Thread對同一個變數操作的話,可能有不可預期的結果。

以下這段Code哪裡有問題?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class RedPimple extends Thread {
private boolean done;
private int value;
@Override
public void run() {
while (!done) //A
Thread.yield();
System.out.println(value); // D
}
public void done() {
done = true;
}
public void setValue(int value) {
this.value = value;
}
public static void main(String[] args) {
RedPimple r = new RedPimple();
r.start();
r.setValue(1); // B
r.done(); // C
}
}

上面Code有兩個Thread,Main Thread跟r。在Main Thread那邊進行value跟done的修改,不一定會被r thread所看到。因此可能會無法結束,或是value為0的情況發生。不過這樣子在寫MultiThread的時候還是有所欠缺。拿最多人討論的https://www.youtube.com/watch?v=1j_mpwKmlJg來說,因此改變了JDK5之嘔的Memory Model,也跟著啟發了C/C++11的Memory Model。
為什麼以下這段Code是錯的

1
2
3
4
5
6
7
8
9
10
11
12
class Foo { 
private Helper helper = null;
public Helper getHelper() {
if (helper == null) // A
synchronized(this) {
if (helper == null)
helper = new Helper();
}
return helper;
}
// other functions and members...
}

其中的helper = new Helper()其實是以下述句組成。

1
2
3
Allocate memory to tmp  // B
Invoke Helper constructor from tmp // C
Assin helper to tmp; // D

在JDK5之前,沒有任何保證以上的述句會按照想像的順序執行,就算編譯器繞路避掉,CPU也有本事做同樣的事,可能C的執行順序在D之後。當一個Thread先執行D,卻捱沒初始化完成,另一個Thread剛好跑到’A’的點,拿到一個可能有問題的物件。就出事了。
因此語言創造者提出了Happens-Before Relation Model。主要的兩點是,每個語言的作法不盡相同,不過大都依循Acquire and Release Semantics來發展。 Java的volatile就擁有Acquire and Release Semantics。而兩者的定義如下:

A read-acquire executes before all reads and writes the same thread that follow it in program order
A write-release executes after all reads and writes by the same thread that precede it in program order

也就是兩種特殊的Memory Fence,看比較容易理解。

依照Program Order來看,在Read-acquire Fence後的讀寫不允許在Fence之前執行,不過在此之前的讀寫允許放到Fence之後進行。
依照Program Order來看,在Write-acquire Fence後的讀寫不允許在Fence之後執行,不過在此之後嘔讀寫允許放到Fence之前進行。

Java修改了Memory model,因此可以保證上面的BC一定會在D之前執行完畢。而在另一個Thread執行到A時,所拿到的helper是正確初始化的。

Visual C++從2005之後對volatile加上了Acquire and Release Semantics,不過這編有個VC失敗的範例,未來可能會修正吧。

Visual C++對volatile做了語義上的拓展,不過GCC跟Clang沒有,所以你不能保證任哦東西。
至於C11跟C++11,則是另起爐灶,用之前介紹的Atomic funtions來做,可以參考這篇

C#的用法就跟Java差不多了,有興趣的襪可以參考Volatile keyword in C# – memory model explained

寫道這邊已經語無倫次了,搞懂這些還真是複雜。

Referene: