ConcurrentDictionary線程不安全么

話題

本節(jié)的內(nèi)容算是非常老的一個(gè)知識(shí)點(diǎn),在.NET4.0中就已經(jīng)出現(xiàn),并且在園中已有園友作出了一定分析,為何我又拿出來講呢?理由如下:

創(chuàng)新互聯(lián)堅(jiān)信:善待客戶,將會(huì)成為終身客戶。我們能堅(jiān)持多年,是因?yàn)槲覀円恢笨芍档眯刨嚒N覀儚牟缓鲇瞥踉L客戶,我們用心做好本職工作,不忘初心,方得始終。10余年網(wǎng)站建設(shè)經(jīng)驗(yàn)創(chuàng)新互聯(lián)是成都老牌網(wǎng)站營(yíng)銷服務(wù)商,為您提供成都網(wǎng)站制作、網(wǎng)站設(shè)計(jì)、網(wǎng)站設(shè)計(jì)、H5頁(yè)面制作、網(wǎng)站制作、品牌網(wǎng)站設(shè)計(jì)、微信小程序開發(fā)服務(wù),給眾多知名企業(yè)提供過好品質(zhì)的建站服務(wù)。

(1)沒用到過,算是自己的一次切身學(xué)習(xí)。

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

(3)是否能夠讓讀者理解的更加透徹呢?打不打臉不要緊,重要的是學(xué)習(xí)的過程和心得。

在.NET1.0中出現(xiàn)了HashTable這個(gè)類,此類不是線程安全的,后來為了線程安全又有了Hashtable.Synchronized,之前看到同事用Hashtable.Synchronized來進(jìn)行實(shí)體類與數(shù)據(jù)庫(kù)中的表進(jìn)行映射,緊接著又看到別的項(xiàng)目中有同事用ConcurrentDictionary類來進(jìn)行映射,一查資料又發(fā)現(xiàn)Hashtable.Synchronized并不是真正的線程安全,至此才引起我的疑惑,于是決定一探究竟, 園中已有大篇文章說ConcurrentDictionary類不是線程安全的。為什么說是線程不安全的呢?至少我們首先得知道什么是線程安全,看看其定義是怎樣的。定義如下:

線程安全:如果你的代碼所在的進(jìn)程中有多個(gè)線程在同時(shí)運(yùn)行,而這些線程可能會(huì)同時(shí)運(yùn)行這段代碼。如果每次運(yùn)行結(jié)果和單線程運(yùn)行的結(jié)果是一樣的,而且其他的變量的值也和預(yù)期的是一樣的,就是線程安全的。

一搜索線程安全比較統(tǒng)一的定義就是上述所給出的,園中大部分對(duì)于此類中的GetOrAdd或者AddOrUpdate參數(shù)含有委托的方法覺得是線程不安全的,我們上述也給出線程安全的定義,現(xiàn)在我們來看看其中之一。

ConcurrentDictionary線程不安全么

        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);
        }

ConcurrentDictionary線程不安全么

對(duì)于GetOrAdd方法它是怎樣知道數(shù)據(jù)應(yīng)該是添加還是獲取呢?該方法描述如下:

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

當(dāng)給出指定鍵時(shí),會(huì)去進(jìn)行遍歷若存在直接返回其值,若不存在此時(shí)會(huì)調(diào)用第二個(gè)參數(shù)也就是委托將運(yùn)行,并將其添加到字典中,最終返回給調(diào)用者此鍵對(duì)應(yīng)的值。

此時(shí)運(yùn)行上述程序我們會(huì)得到如下二者之一的結(jié)果:

ConcurrentDictionary線程不安全么

ConcurrentDictionary線程不安全么

我們開啟兩個(gè)線程,上述運(yùn)行結(jié)果不都是一樣的么, 按照上述定義應(yīng)該是線程安全才對(duì)啊,好了到了這里關(guān)于線程安全的定義我們應(yīng)該消除以下兩點(diǎn)才算是真正的線程安全。

(1)競(jìng)爭(zhēng)條件

(2)死鎖

那么問題來了,什么又是競(jìng)爭(zhēng)條件呢?好吧,我是傳說中的十萬(wàn)個(gè)什么。

就像女朋友說的哪有這么多為什么,我說的都是對(duì)的,不要問為什么,但對(duì)于這么嚴(yán)謹(jǐn)?shù)氖虑?,我們得?shí)事求是,是不。競(jìng)爭(zhēng)條件是軟件或者系統(tǒng)中的一種行為,它的輸出不會(huì)受到其他事件的影響而影響,若因事件受到影響,如果事件未發(fā)生則后果很嚴(yán)重,繼而產(chǎn)生bug諾。 最常見的場(chǎng)景發(fā)生在當(dāng)有兩個(gè)線程同時(shí)共享一個(gè)變量時(shí),一個(gè)線程在讀這個(gè)變量,而另外一個(gè)變量同時(shí)在寫這個(gè)變量。比如定義一個(gè)變量初始化為0,現(xiàn)在有兩個(gè)線程共享此變量,此時(shí)有一個(gè)線程操作將其增加1,同時(shí)另外一個(gè)線程操作也將其增加1此時(shí)此時(shí)得到的結(jié)果將是1,而實(shí)際上我們期待的結(jié)果應(yīng)該是2,所以為了解決競(jìng)爭(zhēng)我們通過用鎖機(jī)制來實(shí)現(xiàn)在多線程環(huán)境下的線程安全。

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

至于死鎖則不用多講,死鎖發(fā)生在多線程或者并發(fā)環(huán)境下,為了等待其他操作完成,但是其他操作一直遲遲未完成從而造成死鎖情況。滿足什么條件才會(huì)引起死鎖呢?如下:

(1)互斥:只有進(jìn)程在給定的時(shí)間內(nèi)使用資源。

(2)占用并等待。

(3)不可搶先。

(4)循環(huán)等待。

到了這里我們通過對(duì)線程安全的理解明白一般為了線程安全都會(huì)加鎖來進(jìn)行處理,而在ConcurrentDictionary中參數(shù)含有委托的方法并未加鎖,但是結(jié)果依然是一樣的,至于未加鎖說是為了出現(xiàn)其他不可預(yù)料的情況,依據(jù)我個(gè)人理解并非完全線程不安全,只是對(duì)于多線程環(huán)境下有可能出現(xiàn)數(shù)據(jù)不一致的情況,為什么說數(shù)據(jù)不一致呢?我們繼續(xù)向下探討。我們將上述方法進(jìn)行修改如下:

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);
        }

ConcurrentDictionary線程不安全么

主程序輸出運(yùn)行次數(shù):

ConcurrentDictionary線程不安全么

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

            PrintValue("JeffckyWang from cnblogs");

            Console.WriteLine(string.Format("運(yùn)行次數(shù)為:{0}", _runCount));

ConcurrentDictionary線程不安全么

ConcurrentDictionary線程不安全么

此時(shí)我們看到確確實(shí)實(shí)獲得了相同的值,但是卻運(yùn)行了兩次,為什么會(huì)運(yùn)行兩次,此時(shí)第二個(gè)線程在運(yùn)行調(diào)用之前,而第一個(gè)線程的值還未進(jìn)行保存而導(dǎo)致。整個(gè)情況大致可以進(jìn)行如下描述:

(1)線程1調(diào)用GetOrAdd方法時(shí),此鍵不存在,此時(shí)會(huì)調(diào)用valueFactory這個(gè)委托。

(2)線程2也調(diào)用GetOrAdd方法,此時(shí)線程1還未完成,此時(shí)也會(huì)調(diào)用valueFactory這個(gè)委托。

(3)線程1完成調(diào)用,并返回JeffckyWang值到字典中,此時(shí)檢查鍵還并未有值,然后將其添加到新的KeyValuePair中,并將JeffckyWang返回給調(diào)用者。

(4)線程2完成調(diào)用,并返回cnblogs值到字典中,此時(shí)檢查此鍵的值已經(jīng)被保存在線程1中,于是中斷添加其值用線程1中的值進(jìn)行代替,最終返回給調(diào)用者。

(5)線程3調(diào)用GetOrAdd方法找到鍵key其值已經(jīng)存在,并返回其值給調(diào)用者,不再調(diào)用valueFactory這個(gè)委托。

從這里我們知道了結(jié)果是一致的,但是運(yùn)行了兩次,其上是三個(gè)線程,若是更多線程,則會(huì)重復(fù)運(yùn)行多次,如此或造成數(shù)據(jù)不一致,所以我的理解是并非完全線程不安全。難道此類中的兩個(gè)方法是線程不安全,.NET團(tuán)隊(duì)沒意識(shí)到么,其實(shí)早就意識(shí)到了,上述也說明了如果為了防止出現(xiàn)意想不到的情況才這樣設(shè)計(jì),說到這里就需要多說兩句,開源最大的好處就是能集思廣益,目前已開源的 Microsoft.AspNetCore.Mvc.Core ,我們可以查看中間件管道源代碼如下:

ConcurrentDictionary線程不安全么

    /// <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線程不安全么

通過ConcurrentDictionary類調(diào)用上述方法無(wú)法保證委托調(diào)用的次數(shù),在對(duì)于mvc中間管道只能初始化一次所以ASP.NET Core團(tuán)隊(duì)使用Lazy<>來初始化,此時(shí)我們將上述也進(jìn)行上述對(duì)應(yīng)的修改,如下:

ConcurrentDictionary線程不安全么

               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);

ConcurrentDictionary線程不安全么

此時(shí)將得到如下:

ConcurrentDictionary線程不安全么

我們將第二個(gè)參數(shù)修改為L(zhǎng)azy<string>,最終調(diào)用valueFound.value將調(diào)用次數(shù)輸出到控制臺(tái)上。此時(shí)我們?cè)賮斫忉屔鲜稣麄€(gè)過程發(fā)生了什么。

(1)線程1調(diào)用GetOrAdd方法時(shí),此鍵不存在,此時(shí)會(huì)調(diào)用valueFactory這個(gè)委托。

(2)線程2也調(diào)用GetOrAdd方法,此時(shí)線程1還未完成,此時(shí)也會(huì)調(diào)用valueFactory這個(gè)委托。

(3)線程1完成調(diào)用,返回一個(gè)未初始化的Lazy<string>對(duì)象,此時(shí)在Lazy<string>對(duì)象上的委托還未進(jìn)行調(diào)用,此時(shí)檢查未存在鍵key的值,于是將Lazy<striing>插入到字典中,并返回給調(diào)用者。

(4)線程2也完成調(diào)用,此時(shí)返回一個(gè)未初始化的Lazy<string>對(duì)象,在此之前檢查到已存在鍵key的值通過線程1被保存到了字典中,所以會(huì)中斷創(chuàng)建,于是其值會(huì)被線程1中的值所代替并返回給調(diào)用者。

(5)線程1調(diào)用Lazy<string>.Value,委托的調(diào)用以線程安全的方式運(yùn)行,所以如果被兩個(gè)線程同時(shí)調(diào)用則只運(yùn)行一次。

(6)線程2調(diào)用Lazy<string>.Value,此時(shí)相同的Lazy<string>剛被線程1初始化過,此時(shí)則不會(huì)再進(jìn)行第二次委托調(diào)用,如果線程1的委托初始化還未完成,此時(shí)線程2將被阻塞,直到完成為止,線程2才進(jìn)行調(diào)用。

(7)線程3調(diào)用GetOrAdd方法,此時(shí)已存在鍵key則不再調(diào)用委托,直接返回鍵key保存的結(jié)果給調(diào)用者。

上述使用Lazy來強(qiáng)迫我們運(yùn)行委托只運(yùn)行一次,如果調(diào)用委托比較耗時(shí)此時(shí)不利用Lazy來實(shí)現(xiàn)那么將調(diào)用多次,結(jié)果可想而知,現(xiàn)在我們只需要運(yùn)行一次,雖然二者結(jié)果是一樣的。我們通過調(diào)用Lazy<string>.Value來促使委托以線程安全的方式運(yùn)行,從而保證在某一個(gè)時(shí)刻只有一個(gè)線程在運(yùn)行,其他調(diào)用Lazy<string>.Value將會(huì)被阻塞直到第一個(gè)調(diào)用執(zhí)行完,其余的線程將使用相同的結(jié)果。

那么問題來了調(diào)用Lazy<>.Value為何是線程安全的呢? 

我們接下來看看Lazy對(duì)象。方便演示我們定義一個(gè)博客類

ConcurrentDictionary線程不安全么

    public class Blog
    {        public string BlogName { get; set; }        public Blog()
        {
            Console.WriteLine("博客構(gòu)造函數(shù)被調(diào)用");
            BlogName = "JeffckyWang";
        }
    }

ConcurrentDictionary線程不安全么

接下來在控制臺(tái)進(jìn)行調(diào)用:

ConcurrentDictionary線程不安全么

            var blog = new Lazy<Blog>();
            Console.WriteLine("博客對(duì)象被定義");            if (!blog.IsValueCreated) Console.WriteLine("博客對(duì)象還未被初始化");
            Console.WriteLine("博客名稱為:" + (blog.Value as Blog).BlogName);            if (blog.IsValueCreated) 
                Console.WriteLine("博客對(duì)象現(xiàn)在已經(jīng)被初始化完畢");

ConcurrentDictionary線程不安全么

打印如下:

ConcurrentDictionary線程不安全么

通過上述打印我們知道當(dāng)調(diào)用blog.Value時(shí),此時(shí)博客對(duì)象才被創(chuàng)建并返回對(duì)象中的屬性字段的值,上述布爾屬性即IsValueCreated顯示表明Lazy對(duì)象是否已經(jīng)被初始化,上述初始化對(duì)象過程可以簡(jiǎn)述如下:

ConcurrentDictionary線程不安全么

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

ConcurrentDictionary線程不安全么

打印結(jié)果和上述一致。上述運(yùn)行都是在非線程安全的模式下進(jìn)行,要是在多線程環(huán)境下對(duì)象只被創(chuàng)建一次我們需要用到如下構(gòu)造函數(shù):

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

通過指定LazyThreadSafetyMode的枚舉值來進(jìn)行。

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

(2)PublicationOnly = 1【針對(duì)于多線程,有多個(gè)線程運(yùn)行初始化方法時(shí),當(dāng)?shù)谝粋€(gè)線程完成時(shí)其值則會(huì)設(shè)置到其他線程】

(3)ExecutionAndPublication = 2【針對(duì)單線程,加鎖機(jī)制,每個(gè)初始化方法執(zhí)行完畢,其值則相應(yīng)的輸出】

我們演示下情況:

ConcurrentDictionary線程不安全么

    public class Blog
    {        public int BlogId { get; set; }        public Blog()
        {
            Console.WriteLine("博客構(gòu)造函數(shù)被調(diào)用");
        }
    }

ConcurrentDictionary線程不安全么

ConcurrentDictionary線程不安全么

        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);

        }

ConcurrentDictionary線程不安全么

ConcurrentDictionary線程不安全么

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

ConcurrentDictionary線程不安全么

結(jié)果打印如下:

ConcurrentDictionary線程不安全么

奇怪的是當(dāng)改變線程安全模式為 LazyThreadSafetyMode.ExecutionAndPublication 時(shí)結(jié)果應(yīng)該為101和102才是,居然返回的都是102,但是將上述blog.BogId++和暫停時(shí)間順序顛倒時(shí)如下:

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

此時(shí)兩個(gè)模式返回的都是101和102,不知是何緣故!上述在ConcurrentDictionary類中為了兩個(gè)方法能保證線程安全我們利用Lazy來實(shí)現(xiàn),默認(rèn)的模式為 LazyThreadSafetyMode.ExecutionAndPublication 保證委托只執(zhí)行一次。為了不破壞原生調(diào)用ConcurrentDictionary的GetOrAdd方法,但是又為了保證線程安全,我們封裝一個(gè)方法來方便進(jìn)行調(diào)用。

ConcurrentDictionary線程不安全么

        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;
            }
        }

ConcurrentDictionary線程不安全么

原封不動(dòng)的進(jìn)行方法調(diào)用:

ConcurrentDictionary線程不安全么

           _runCount =    LazyConcurrentDictionary<, >=  LazyConcurrentDictionary<, >   Main( task1 = Task.Run(() => PrintValue( task2 = Task.Run(() => PrintValue(.Format(   PrintValue( valueFound = _lazyDictionary.GetOrAdd(=>

ConcurrentDictionary線程不安全么

最終正確打印只運(yùn)行一次的結(jié)果,如下:

ConcurrentDictionary線程不安全么

名稱欄目:ConcurrentDictionary線程不安全么
當(dāng)前鏈接:http://muchs.cn/article0/jcpsio.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供小程序開發(fā)、商城網(wǎng)站、外貿(mào)建站、移動(dòng)網(wǎng)站建設(shè)、定制網(wǎng)站、外貿(mào)網(wǎng)站建設(shè)

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)

成都seo排名網(wǎng)站優(yōu)化