[C#.NET 拾遺補漏]10:理解 volatile 關鍵字

精緻碼農 發表於 2020-10-28

要理解 C# 中的 volatile 關鍵字,就要先知道編譯器背後的一個基本優化原理。比如對於下面這段程式碼:

public class Example
{
    public int x;
    public void DoWork()
    {
        x = 5;
        var y = x + 10;
        Debug.WriteLine("x = " +x + ", y = " +y);
    }
}

在 Release 模式下,編譯器讀取 x = 5 後緊接著讀取 y = x + 10,在單執行緒思維模式下,編譯器會認為 y 的值始終都是 15。所以編譯器會把 y = x + 10 優化為 y = 15,避免每次讀取 y 都執行一次 x + 5。但 x 欄位的值可能在執行時被其它的執行緒修改,我們拿到的 y 值並不是通過最新修改的 x 計算得來的,y 的值永遠都是 15

也就是說,編譯器在 Release 模式下會對欄位的訪問進行優化,它假定欄位都是由單個執行緒訪問的,把與該欄位相關的表示式運算結果編譯成常量快取起來,避免每次訪問都重複運算。但這樣就可能導致其它執行緒修改了欄位值而當前執行緒卻讀取不到最新的欄位值。為了防止編譯器這麼做,你就要讓編譯器用多執行緒思維去解讀程式碼。告訴編譯器欄位的值可能會被其它執行緒修改,這種情況不要使用優化策略。而要做到這一點,就需要使用 volatile 關鍵字。

給類的欄位新增 volatile 關鍵字,目的是告訴編譯器該欄位的值可能會被多個獨立的執行緒改變,不要對該欄位的訪問進行優化。

使用 volatile 可以確保欄位的值是可用的最新值,而且該值不會像非 volatile 欄位值那樣受到快取的影響。好的做法是將每個可能被多個執行緒使用的欄位標記為 volatile,以防止非預期的優化行為。

為了加深理解,我們來看一個實際的例子:

public class Worker
{
    private bool _shouldStop;

    public void DoWork()
    {
        bool work = false;
        // 注意:這裡會被編譯器優化為 while(true)
        while (!_shouldStop)
        {
            work = !work; // do sth.
        }
        Console.WriteLine("工作執行緒:正在終止...");
    }

    public void RequestStop()
    {
        _shouldStop = true;
    }
}

public class Program
{
    public static void Main()
    {
        var worker = new Worker();

        Console.WriteLine("主執行緒:啟動工作執行緒...");
        var workerTask = Task.Run(worker.DoWork);

        // 等待 500 毫秒以確保工作執行緒已在執行
        Thread.Sleep(500);

        Console.WriteLine("主執行緒:請求終止工作執行緒...");
        worker.RequestStop();

        // 待待工作執行緒執行結束
        workerTask.Wait();
        //workerThread.Join();

        Console.WriteLine("主執行緒:工作執行緒已終止");
    }
}

在這個例子中,while (!_shouldStop) 會被編譯器優化為 while(true)。我們可以看一下實際的執行效果來驗證這一點。切換 Release 模式,按 Ctrl + F5 執行程式,執行效果始終如下:

[C#.NET 拾遺補漏]10:理解 volatile 關鍵字

程式執行後,雖然主執行緒在 500 毫秒後執行 RequestStop() 方法修改了 _shouldStop 的值,但工作執行緒始終都獲取不到 _shouldStop 最新的值,也就永遠都不會終止 while 迴圈。

我們修改一下程式,對 _shouldStop 欄位加上 volatile 關鍵字:

public class Worker
{
    private volatile bool _shouldStop;

    public void DoWork()
    {
        bool work = false;
        // 獲取的是最新的 _shouldStop 值
        while (!_shouldStop)
        {
            work = !work; // do sth.
        }
        Console.WriteLine("工作執行緒:正在終止...");
    }

    // ...(略)
}

此時在主執行緒呼叫 RequestStop() 方法後,工作執行緒便立即終止了,執行效果如下圖所示:

[C#.NET 拾遺補漏]10:理解 volatile 關鍵字

這說明加了 volatile 關鍵字後,程式可以實時讀取到欄位的最新值。

注意,一定要切換為 Release 模式執行才能看到 volatile 發揮的作用,Debug 模式下即使新增了 volatile 關鍵字,編譯器也是不會執行優化的。

當然,並不是所有的型別都可以使用 volatile 關鍵字修飾的,常見的使用 volatile 的型別是這些簡單型別:sbyte, byte, short, ushort, int, uint, char, float 和 bool,其它的請檢視參考連結。


參考:
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/volatile