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

要理解 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 運行程序,運行效果始終如下:

程序運行後,雖然主線程在 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() 方法後,工作線程便立即終止了,運行效果如下圖所示:

這說明加了 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章