細說.NET中的多線程 (六 使用MemoryBarrier,Volatile進行同步)

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

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。可是有一點須要注意:多線程去修改變量而且不使用任何形似的鎖或者內存柵欄是會帶來必定的麻煩的。blog

下面一個例子可以很好的說明上面的觀點(在你的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相關的使用。

 

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

相關文章
相關標籤/搜索