文章出處

上一節介紹了使用信號量進行同步,本節主要介紹一些非阻塞同步的方法。本節主要介紹MemoryBarrier,volatile,Interlocked。

MemoryBarriers

本文簡單的介紹一下這兩個概念,假設下面的代碼:

using System;
class Foo
{
    int _answer;
    bool _complete;

    void A()
    {
        _answer = 123;
        _complete = true;
    }

    void B()
    {
        if (_complete) Console.WriteLine(_answer);
    }
}

如果方法A和方法B同時在兩個不同線程中運行,控制臺可能輸出0嗎?答案是可能的,有以下兩個原因:

  • 編譯器,CLR或者CPU可能會更改指令的順序來提高性能
  • 編譯器,CLR或者CPU可能會通過緩存來優化變量,這種情況下對其他線程是不可見的。

最簡單的方式就是通過MemoryBarrier來保護變量,來防止任何形式的更改指令順序或者緩存。調用Thread.MemoryBarrier會生成一個內存柵欄,我們可以通過以下的方式解決上面的問題:

using System;
using System.Threading;
class Foo
{
    int _answer;
    bool _complete;

    void A()
    {
        _answer = 123;
        Thread.MemoryBarrier();    // Barrier 1
        _complete = true;
        Thread.MemoryBarrier();    // Barrier 2
    }

    void B()
    {
        Thread.MemoryBarrier();    // Barrier 3
        if (_complete)
        {
            Thread.MemoryBarrier();       // Barrier 4
            Console.WriteLine(_answer);
        }
    }
}

上面的例子中,barrier1和barrier3用來保證指令順序不會改變,barrier2和barrier4用來保證值變化不被緩存。一個好的處理方案就是我們在需要保護的變量前后分別加上MemoryBarrier。

在c#中,下面的操作都會生成MemoryBarrier:

  • Lock語句(Monitor.Enter,Monitor.Exit)
  • 所有Interlocked類的方法
  • 線程池的回調方法
  • Set或者Wait信號
  • 所有依賴于信號燈實現的方法,如starting或waiting 一個Task

因為上面這些行為,這段代碼實際上是線程安全的:

        int x = 0;
        Task t = Task.Factory.StartNew(() => x++);
        t.Wait();
        Console.WriteLine(x);    // 1

在你自己的程序中,你可能重現不出來上面例子所說的情況。事實上,從msdn上對MomoryBarrier的解釋來看,只有對順序保護比較弱的多核系統才需要用到MomoryBarrier。但是有一點需要注意:多線程去修改變量并且不使用任何形似的鎖或者內存柵欄是會帶來一定的麻煩的。

下面一個例子能夠很好的說明上面的觀點(在你的VisualStudio中,選擇Release模式,并且Start Without Debugging重現這個問題):

        bool complete = false;
        var t = new Thread(() =>
        {
            bool toggle = false;
            while (!complete) toggle = !toggle;
        });
        t.Start();
        Thread.Sleep(1000);
        complete = true;
        t.Join();        // Blocks indefinitely

這個程序永遠不會結束,因為complete變量被緩存在了CPU寄存器中。在while循環中加入Thread.MemoryBarrier可以解決這個問題。

volatile關鍵字

另外一種更高級的方式來解決上面的問題,那就是考慮使用volatile關鍵字。Volatile關鍵字告訴編譯器在每一次讀操作時生成一個fence,來實現保護保護變量的目的。具體說明可以參見msdn的介紹

VolatileRead和VolatileWrite

Volatile關鍵字只能加到類變量中。本地變量不能被聲明成volatile。這種情況你可以考慮使用System.Threading.Volatile.Read方法。我們看一下System.Threading.Volatile源碼如何實現這兩個方法的:

    public static bool Read(ref bool location)
    {
        bool flag = location;
        Thread.MemoryBarrier();
        return flag;
    }
    public static void Write(ref bool location, bool value)
    {
        Thread.MemoryBarrier();
        location = value;
    }

  

一目了然,通過MemoryBarrier來實現的,但是他只在讀操作的后面和寫操作的前面加了MemoryBarrier,那么你應該考慮,如果你先使用Volatile.Write再使用Volatile.Read是不是可能有問題呢?

c#中ConcurrentDictionary中使用了Volatile類來保護變量,有興趣的讀者可以看看c#的開發者是如何使用這個方法來保護變量的。

Interlocked

使用MemoryBarrier并不總是一個好的解決方案,尤其在不需要鎖的情況下。Interlocked方法提供了一些常用的原子操作來避免前面文章提到的一系列的問題。如使用Interlocked.Increment來替代++,Interlocked.Decrement來替代--。Msdn的文檔中詳細的介紹了相關的用法和原理。C#中的源碼里也經常能看見Interlocked相關的使用。

 

本文介紹了一些除了鎖和信號量之外的一些同步方式,歡迎批評與指正。


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()