ConcurrentDictionary線程不安全麼,你難道沒疑惑,你難道弄懂了麼?

前言

事情不太多時,會時不時去看項目中同事寫的代碼能夠做個參考或者學習,我的以爲只有這樣才能走的更遠,抱着一副老子天下第一的態度最終只能是井底之蛙。前兩篇寫到關於斷點傳續的文章,還有一篇還未寫出,後續會補上,這裏咱們穿插一篇文章,這是我看到同事寫的代碼中有ConcurrentDictionary這個類,以前並未接觸過,就深刻了解了一下,因此算是查漏補缺,基礎拾遺吧,想要學習的這種勁頭越有,你會發覺忽然涌現的知識越多,學無止境!。數據庫

話題

本節的內容算是很是老的一個知識點,在.NET4.0中就已經出現,而且在園中已有園友做出了必定分析,爲什麼我又拿出來說呢?理由以下:安全

(1)沒用到過,算是本身的一次切身學習。多線程

(2)對比一下園友所述,我想我是否能講的更加詳盡呢?挑戰一下。併發

(3)是否可以讓讀者理解的更加透徹呢?打不打臉沒關係,重要的是學習的過程和心得。mvc

在.NET1.0中出現了HashTable這個類,此類不是線程安全的,後來爲了線程安全又有了Hashtable.Synchronized,以前看到同事用Hashtable.Synchronized來進行實體類與數據庫中的表進行映射,緊接着又看到別的項目中有同事用ConcurrentDictionary類來進行映射,一查資料又發現Hashtable.Synchronized並非真正的線程安全,至此才引發個人疑惑,因而決定一探究竟, 園中已有大篇文章說ConcurrentDictionary類不是線程安全的。爲何說是線程不安全的呢?至少咱們首先得知道什麼是線程安全,看看其定義是怎樣的。定義以下:ide

線程安全:若是你的代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。若是每次運行結果和單線程運行的結果是同樣的,並且其餘的變量的值也和預期的是同樣的,就是線程安全的。函數

一搜索線程安全比較統一的定義就是上述所給出的,園中大部分對於此類中的GetOrAdd或者AddOrUpdate參數含有委託的方法以爲是線程不安全的,咱們上述也給出線程安全的定義,如今咱們來看看其中之一。學習

        private static readonly ConcurrentDictionary<string, string> _dictionary
            = new ConcurrentDictionary<string, string>();

        public static void Main(string[] args)
        {
            var task1 = Task.Run(() => PrintValue("JeffckWang"));
            var task2 = Task.Run(() => PrintValue("cnblogs"));
            Task.WaitAll(task1, task2);

            PrintValue("JeffckyWang from cnblogs");
            Console.ReadKey();
        }

        public static void PrintValue(string valueToPrint)
        {
            var valueFound = _dictionary.GetOrAdd("key",
                        x =>
                        {
                            return valueToPrint;
                        });
            Console.WriteLine(valueFound);
        }

對於GetOrAdd方法它是怎樣知道數據應該是添加仍是獲取呢?該方法描述以下:ui

TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory);  

當給出指定鍵時,會去進行遍歷若存在直接返回其值,若不存在此時會調用第二個參數也就是委託將運行,並將其添加到字典中,最終返回給調用者此鍵對應的值。this

此時運行上述程序咱們會獲得以下兩者之一的結果:

咱們開啓兩個線程,上述運行結果不都是同樣的麼, 按照上述定義應該是線程安全才對啊,好了到了這裏關於線程安全的定義咱們應該消除如下兩點纔算是真正的線程安全。

(1)競爭條件

(2)死鎖

那麼問題來了,什麼又是競爭條件呢?好吧,我是傳說中的十萬個什麼。

就像女友說的哪有這麼多爲何,我說的都是對的,不要問爲何,但對於這麼嚴謹的事情,咱們得實事求是,是不。競爭條件是軟件或者系統中的一種行爲,它的輸出不會受到其餘事件的影響而影響,若因事件受到影響,若是事件未發生則後果很嚴重,繼而產生bug諾。 最多見的場景發生在當有兩個線程同時共享一個變量時,一個線程在讀這個變量,而另一個變量同時在寫這個變量。好比定義一個變量初始化爲0,如今有兩個線程共享此變量,此時有一個線程操做將其增長1,同時另一個線程操做也將其增長1此時此時獲得的結果將是1,而實際上咱們期待的結果應該是2,因此爲了解決競爭咱們經過用鎖機制來實如今多線程環境下的線程安全。

那麼問題來了,什麼是死鎖呢?

至於死鎖則不用多講,死鎖發生在多線程或者併發環境下,爲了等待其餘操做完成,可是其餘操做一直遲遲未完成從而形成死鎖狀況。知足什麼條件纔會引發死鎖呢?以下:

(1)互斥:只有進程在給定的時間內使用資源。

(2)佔用並等待。

(3)不可搶先。

(4)循環等待。

到了這裏咱們經過對線程安全的理解明白通常爲了線程安全都會加鎖來進行處理,而在ConcurrentDictionary中參數含有委託的方法並未加鎖,可是結果依然是同樣的,至於未加鎖說是爲了出現其餘不可預料的狀況,依據我我的理解並不是徹底線程不安全,只是對於多線程環境下有可能出現數據不一致的狀況,爲何說數據不一致呢?咱們繼續向下探討。咱們將上述方法進行修改以下:

        public static void PrintValue(string valueToPrint)
        {
            var valueFound = _dictionary.GetOrAdd("key",
                   x =>
                   {
                       Interlocked.Increment(ref _runCount);
                       Thread.Sleep(100);
                       return valueToPrint;
                   });
            Console.WriteLine(valueFound);
        }

主程序輸出運行次數:

            var task1 = Task.Run(() => PrintValue("JeffckyWang"));
            var task2 = Task.Run(() => PrintValue("cnblogs"));
            Task.WaitAll(task1, task2);

            PrintValue("JeffckyWang from cnblogs");

            Console.WriteLine(string.Format("運行次數爲:{0}", _runCount));

此時咱們看到確確實實得到了相同的值,可是卻運行了兩次,爲何會運行兩次,此時第二個線程在運行調用以前,而第一個線程的值還未進行保存而致使。整個狀況大體能夠進行以下描述:

(1)線程1調用GetOrAdd方法時,此鍵不存在,此時會調用valueFactory這個委託。

(2)線程2也調用GetOrAdd方法,此時線程1還未完成,此時也會調用valueFactory這個委託。

(3)線程1完成調用,並返回JeffckyWang值到字典中,此時檢查鍵還並未有值,而後將其添加到新的KeyValuePair中,並將JeffckyWang返回給調用者。

(4)線程2完成調用,並返回cnblogs值到字典中,此時檢查此鍵的值已經被保存在線程1中,因而中斷添加其值用線程1中的值進行代替,最終返回給調用者。

(5)線程3調用GetOrAdd方法找到鍵key其值已經存在,並返回其值給調用者,再也不調用valueFactory這個委託。

從這裏咱們知道告終果是一致的,可是運行了兩次,其上是三個線程,如果更多線程,則會重複運行屢次,如此或形成數據不一致,因此個人理解是並不是徹底線程不安全。難道此類中的兩個方法是線程不安全,.NET團隊沒意識到麼,其實早就意識到了,上述也說明了若是爲了防止出現意想不到的狀況才這樣設計,說到這裏就須要多說兩句,開源最大的好處就是能集思廣益,目前已開源的 Microsoft.AspNetCore.Mvc.Core ,咱們能夠查看中間件管道源代碼以下:

    /// <summary>
    /// Builds a middleware pipeline after receiving the pipeline from a pipeline provider
    /// </summary>
    public class MiddlewareFilterBuilder
    {
        // 'GetOrAdd' call on the dictionary is not thread safe and we might end up creating the pipeline more
        // once. To prevent this Lazy<> is used. In the worst case multiple Lazy<> objects are created for multiple
        // threads but only one of the objects succeeds in creating a pipeline.
        private readonly ConcurrentDictionary<Type, Lazy<RequestDelegate>> _pipelinesCache
            = new ConcurrentDictionary<Type, Lazy<RequestDelegate>>();
        private readonly MiddlewareFilterConfigurationProvider _configurationProvider;

        public IApplicationBuilder ApplicationBuilder { get; set; }
   }

經過ConcurrentDictionary類調用上述方法沒法保證委託調用的次數,在對於mvc中間管道只能初始化一次因此ASP.NET Core團隊使用Lazy<>來初始化,此時咱們將上述也進行上述對應的修改,以下:

               private static readonly ConcurrentDictionary<string, Lazy<string>> _lazyDictionary
            = new ConcurrentDictionary<string, Lazy<string>>();


                var valueFound = _lazyDictionary.GetOrAdd("key",
                x => new Lazy<string>(
                    () =>
                    {
                        Interlocked.Increment(ref _runCount);
                        Thread.Sleep(100);
                        return valueToPrint;
                    }));
                Console.WriteLine(valueFound.Value);

此時將獲得以下:

咱們將第二個參數修改成Lazy<string>,最終調用valueFound.value將調用次數輸出到控制檯上。此時咱們再來解釋上述整個過程發生了什麼。

(1)線程1調用GetOrAdd方法時,此鍵不存在,此時會調用valueFactory這個委託。

(2)線程2也調用GetOrAdd方法,此時線程1還未完成,此時也會調用valueFactory這個委託。

(3)線程1完成調用,返回一個未初始化的Lazy<string>對象,此時在Lazy<string>對象上的委託還未進行調用,此時檢查未存在鍵key的值,因而將Lazy<striing>插入到字典中,並返回給調用者。

(4)線程2也完成調用,此時返回一個未初始化的Lazy<string>對象,在此以前檢查到已存在鍵key的值經過線程1被保存到了字典中,因此會中斷建立,因而其值會被線程1中的值所代替並返回給調用者。

(5)線程1調用Lazy<string>.Value,委託的調用以線程安全的方式運行,因此若是被兩個線程同時調用則只運行一次。

(6)線程2調用Lazy<string>.Value,此時相同的Lazy<string>剛被線程1初始化過,此時則不會再進行第二次委託調用,若是線程1的委託初始化還未完成,此時線程2將被阻塞,直到完成爲止,線程2才進行調用。

(7)線程3調用GetOrAdd方法,此時已存在鍵key則再也不調用委託,直接返回鍵key保存的結果給調用者。

上述使用Lazy來強迫咱們運行委託只運行一次,若是調用委託比較耗時此時不利用Lazy來實現那麼將調用屢次,結果可想而知,如今咱們只須要運行一次,雖然兩者結果是同樣的。咱們經過調用Lazy<string>.Value來促使委託以線程安全的方式運行,從而保證在某一個時刻只有一個線程在運行,其餘調用Lazy<string>.Value將會被阻塞直到第一個調用執行完,其他的線程將使用相同的結果。

那麼問題來了調用Lazy<>.Value爲什麼是線程安全的呢? 

咱們接下來看看Lazy對象。方便演示咱們定義一個博客類

    public class Blog
    {
        public string BlogName { get; set; }

        public Blog()
        {
            Console.WriteLine("博客構造函數被調用");
            BlogName = "JeffckyWang";
        }
    }

接下來在控制檯進行調用:

            var blog = new Lazy<Blog>();
            Console.WriteLine("博客對象被定義");
            if (!blog.IsValueCreated) Console.WriteLine("博客對象還未被初始化");
            Console.WriteLine("博客名稱爲:" + (blog.Value as Blog).BlogName);
            if (blog.IsValueCreated) 
                Console.WriteLine("博客對象如今已經被初始化完畢");

打印以下:

經過上述打印咱們知道當調用blog.Value時,此時博客對象才被建立並返回對象中的屬性字段的值,上述布爾屬性即IsValueCreated顯示代表Lazy對象是否已經被初始化,上述初始化對象過程能夠簡述以下:

            var lazyBlog = new Lazy<Blog>
            (
                () =>
                {
                    var blogObj = new Blog() { BlogName = "JeffckyWang" };
                    return blogObj;
                }
            );

打印結果和上述一致。上述運行都是在非線程安全的模式下進行,要是在多線程環境下對象只被建立一次咱們須要用到以下構造函數:

 public Lazy(LazyThreadSafetyMode mode);
 public Lazy(Func<T> valueFactory, LazyThreadSafetyMode mode);

經過指定LazyThreadSafetyMode的枚舉值來進行。

(1)None = 0【線程不安全】

(2)PublicationOnly = 1【針對於多線程,有多個線程運行初始化方法時,當第一個線程完成時其值則會設置到其餘線程】

(3)ExecutionAndPublication = 2【針對單線程,加鎖機制,每一個初始化方法執行完畢,其值則相應的輸出】

咱們演示下狀況:

    public class Blog
    {
        public int BlogId { get; set; }
        public Blog()
        {
            Console.WriteLine("博客構造函數被調用");
        }
    }
        static void Run(object obj)
        {
            var blogLazy = obj as Lazy<Blog>;
            var blog = blogLazy.Value as Blog;
            blog.BlogId++;
            Thread.Sleep(100);
            Console.WriteLine("博客Id爲:" + blog.BlogId);

        }
            var lazyBlog = new Lazy<Blog>
            (
                () =>
                {
                    var blogObj = new Blog() { BlogId = 100 };
                    return blogObj;
                }, LazyThreadSafetyMode.PublicationOnly
            );
            Console.WriteLine("博客對象被定義");
            ThreadPool.QueueUserWorkItem(new WaitCallback(Run), lazyBlog);
            ThreadPool.QueueUserWorkItem(new WaitCallback(Run), lazyBlog);

結果打印以下:

奇怪的是當改變線程安全模式爲 LazyThreadSafetyMode.ExecutionAndPublication 時結果應該爲101和102纔是,竟然返回的都是102,可是將上述blog.BogId++和暫停時間順序顛倒時以下:

  Thread.Sleep(100);          
  blog.BlogId++;
          

此時兩個模式返回的都是101和102,不知是何緣故!上述在ConcurrentDictionary類中爲了兩個方法能保證線程安全咱們利用Lazy來實現,默認的模式爲 LazyThreadSafetyMode.ExecutionAndPublication 保證委託只執行一次。爲了避免破壞原生調用ConcurrentDictionary的GetOrAdd方法,可是又爲了保證線程安全,咱們封裝一個方法來方便進行調用。

        public class LazyConcurrentDictionary<TKey, TValue>
        {
            private readonly ConcurrentDictionary<TKey, Lazy<TValue>> concurrentDictionary;

            public LazyConcurrentDictionary()
            {
                this.concurrentDictionary = new ConcurrentDictionary<TKey, Lazy<TValue>>();
            }

            public TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory)
            {
                var lazyResult = this.concurrentDictionary.GetOrAdd(key, k => new Lazy<TValue>(() => valueFactory(k), LazyThreadSafetyMode.ExecutionAndPublication));

                return lazyResult.Value;
            }
        }

原封不動的進行方法調用:

        private static int _runCount = 0;
        private static readonly LazyConcurrentDictionary<string, string> _lazyDictionary
              = new LazyConcurrentDictionary<string, string>();

        public static void Main(string[] args)
        {
var task1 = Task.Run(() => PrintValue("JeffckyWang")); var task2 = Task.Run(() => PrintValue("cnblogs")); Task.WaitAll(task1, task2); PrintValue("JeffckyWang from cnblogs"); Console.WriteLine(string.Format("運行次數爲:{0}", _runCount)); Console.Read(); } public static void PrintValue(string valueToPrint) { var valueFound = _lazyDictionary.GetOrAdd("key", x => { Interlocked.Increment(ref _runCount); Thread.Sleep(100); return valueToPrint; }); Console.WriteLine(valueFound); }

最終正確打印只運行一次的結果,以下:

總結

本節咱們學習了ConcurrentDictionary類裏面有兩個方法嚴格來講非線程安全,可是也能夠獲得相同的結果,若咱們僅僅只是獲得相同的結果且操做不是太耗時其實徹底能夠忽略這一點,若當利用ConcurrentDictionary類中的此兩者方法來作比較耗時的操做,此時就要注意讓其線程安全利用Lazy來保證其只能執行一次,因此對ConcurrentDictionary來講並不是全部狀況都要實現嚴格意義上的線程安全,根據實際場景而定纔是最佳解決方案。時不時多看看別人寫的代碼,漲漲見識,天天積累一點,日子長了就牛逼了!

相關文章
相關標籤/搜索