本節(jié)的內(nèi)容算是非常老的一個知識點(diǎn),在.NET4.0中就已經(jīng)出現(xiàn),并且在園中已有園友作出了一定分析,為何我又拿出來講呢?理由如下:
創(chuàng)新互聯(lián)堅(jiān)信:善待客戶,將會成為終身客戶。我們能堅(jiān)持多年,是因?yàn)槲覀円恢笨芍档眯刨嚒N覀儚牟缓鲇瞥踉L客戶,我們用心做好本職工作,不忘初心,方得始終。10余年網(wǎng)站建設(shè)經(jīng)驗(yàn)創(chuàng)新互聯(lián)是成都老牌網(wǎng)站營銷服務(wù)商,為您提供成都網(wǎng)站制作、網(wǎng)站設(shè)計(jì)、網(wǎng)站設(shè)計(jì)、H5頁面制作、網(wǎng)站制作、品牌網(wǎng)站設(shè)計(jì)、微信小程序開發(fā)服務(wù),給眾多知名企業(yè)提供過好品質(zhì)的建站服務(wù)。
(1)沒用到過,算是自己的一次切身學(xué)習(xí)。
(2)對比一下園友所述,我想我是否能講的更加詳盡呢?挑戰(zhàn)一下。
(3)是否能夠讓讀者理解的更加透徹呢?打不打臉不要緊,重要的是學(xué)習(xí)的過程和心得。
在.NET1.0中出現(xiàn)了HashTable這個類,此類不是線程安全的,后來為了線程安全又有了Hashtable.Synchronized,之前看到同事用Hashtable.Synchronized來進(jìn)行實(shí)體類與數(shù)據(jù)庫中的表進(jìn)行映射,緊接著又看到別的項(xiàng)目中有同事用ConcurrentDictionary類來進(jìn)行映射,一查資料又發(fā)現(xiàn)Hashtable.Synchronized并不是真正的線程安全,至此才引起我的疑惑,于是決定一探究竟, 園中已有大篇文章說ConcurrentDictionary類不是線程安全的。為什么說是線程不安全的呢?至少我們首先得知道什么是線程安全,看看其定義是怎樣的。定義如下:
線程安全:如果你的代碼所在的進(jìn)程中有多個線程在同時運(yùn)行,而這些線程可能會同時運(yùn)行這段代碼。如果每次運(yùn)行結(jié)果和單線程運(yùn)行的結(jié)果是一樣的,而且其他的變量的值也和預(yù)期的是一樣的,就是線程安全的。
一搜索線程安全比較統(tǒng)一的定義就是上述所給出的,園中大部分對于此類中的GetOrAdd或者AddOrUpdate參數(shù)含有委托的方法覺得是線程不安全的,我們上述也給出線程安全的定義,現(xiàn)在我們來看看其中之一。
private static readonly ConcurrentDictionary_dictionary = new ConcurrentDictionary (); 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方法它是怎樣知道數(shù)據(jù)應(yīng)該是添加還是獲取呢?該方法描述如下:
TValue GetOrAdd(TKey key,FuncvalueFactory);
當(dāng)給出指定鍵時,會去進(jìn)行遍歷若存在直接返回其值,若不存在此時會調(diào)用第二個參數(shù)也就是委托將運(yùn)行,并將其添加到字典中,最終返回給調(diào)用者此鍵對應(yīng)的值。
此時運(yùn)行上述程序我們會得到如下二者之一的結(jié)果:
我們開啟兩個線程,上述運(yùn)行結(jié)果不都是一樣的么, 按照上述定義應(yīng)該是線程安全才對啊,好了到了這里關(guān)于線程安全的定義我們應(yīng)該消除以下兩點(diǎn)才算是真正的線程安全。
(1)競爭條件
(2)死鎖
就像女朋友說的哪有這么多為什么,我說的都是對的,不要問為什么,但對于這么嚴(yán)謹(jǐn)?shù)氖虑?,我們得?shí)事求是,是不。競爭條件是軟件或者系統(tǒng)中的一種行為,它的輸出不會受到其他事件的影響而影響,若因事件受到影響,如果事件未發(fā)生則后果很嚴(yán)重,繼而產(chǎn)生bug諾。 最常見的場景發(fā)生在當(dāng)有兩個線程同時共享一個變量時,一個線程在讀這個變量,而另外一個變量同時在寫這個變量。比如定義一個變量初始化為0,現(xiàn)在有兩個線程共享此變量,此時有一個線程操作將其增加1,同時另外一個線程操作也將其增加1此時此時得到的結(jié)果將是1,而實(shí)際上我們期待的結(jié)果應(yīng)該是2,所以為了解決競爭我們通過用鎖機(jī)制來實(shí)現(xiàn)在多線程環(huán)境下的線程安全。
至于死鎖則不用多講,死鎖發(fā)生在多線程或者并發(fā)環(huán)境下,為了等待其他操作完成,但是其他操作一直遲遲未完成從而造成死鎖情況。滿足什么條件才會引起死鎖呢?如下:
(1)互斥:只有進(jìn)程在給定的時間內(nèi)使用資源。
(2)占用并等待。
(3)不可搶先。
(4)循環(huán)等待。
到了這里我們通過對線程安全的理解明白一般為了線程安全都會加鎖來進(jìn)行處理,而在ConcurrentDictionary中參數(shù)含有委托的方法并未加鎖,但是結(jié)果依然是一樣的,至于未加鎖說是為了出現(xiàn)其他不可預(yù)料的情況,依據(jù)我個人理解并非完全線程不安全,只是對于多線程環(huán)境下有可能出現(xiàn)數(shù)據(jù)不一致的情況,為什么說數(shù)據(jù)不一致呢?我們繼續(xù)向下探討。我們將上述方法進(jìn)行修改如下:
public static void PrintValue(string valueToPrint) { var valueFound = _dictionary.GetOrAdd("key", x => { Interlocked.Increment(ref _runCount); Thread.Sleep(100); return valueToPrint; }); Console.WriteLine(valueFound); }
主程序輸出運(yùn)行次數(shù):
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));
此時我們看到確確實(shí)實(shí)獲得了相同的值,但是卻運(yùn)行了兩次,為什么會運(yùn)行兩次,此時第二個線程在運(yùn)行調(diào)用之前,而第一個線程的值還未進(jìn)行保存而導(dǎo)致。整個情況大致可以進(jìn)行如下描述:
(1)線程1調(diào)用GetOrAdd方法時,此鍵不存在,此時會調(diào)用valueFactory這個委托。
(2)線程2也調(diào)用GetOrAdd方法,此時線程1還未完成,此時也會調(diào)用valueFactory這個委托。
(3)線程1完成調(diào)用,并返回JeffckyWang值到字典中,此時檢查鍵還并未有值,然后將其添加到新的KeyValuePair中,并將JeffckyWang返回給調(diào)用者。
(4)線程2完成調(diào)用,并返回cnblogs值到字典中,此時檢查此鍵的值已經(jīng)被保存在線程1中,于是中斷添加其值用線程1中的值進(jìn)行代替,最終返回給調(diào)用者。
(5)線程3調(diào)用GetOrAdd方法找到鍵key其值已經(jīng)存在,并返回其值給調(diào)用者,不再調(diào)用valueFactory這個委托。
從這里我們知道了結(jié)果是一致的,但是運(yùn)行了兩次,其上是三個線程,若是更多線程,則會重復(fù)運(yùn)行多次,如此或造成數(shù)據(jù)不一致,所以我的理解是并非完全線程不安全。難道此類中的兩個方法是線程不安全,.NET團(tuán)隊(duì)沒意識到么,其實(shí)早就意識到了,上述也說明了如果為了防止出現(xiàn)意想不到的情況才這樣設(shè)計(jì),說到這里就需要多說兩句,開源最大的好處就是能集思廣益,目前已開源的 Microsoft.AspNetCore.Mvc.Core ,我們可以查看中間件管道源代碼如下:
////// Builds a middleware pipeline after receiving the pipeline from a pipeline provider /// 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> _pipelinesCache = new ConcurrentDictionary >(); private readonly MiddlewareFilterConfigurationProvider _configurationProvider; public IApplicationBuilder ApplicationBuilder { get; set; } }
通過ConcurrentDictionary類調(diào)用上述方法無法保證委托調(diào)用的次數(shù),在對于mvc中間管道只能初始化一次所以ASP.NET Core團(tuán)隊(duì)使用Lazy<>來初始化,此時我們將上述也進(jìn)行上述對應(yīng)的修改,如下:
private static readonly ConcurrentDictionary> _lazyDictionary = new ConcurrentDictionary >(); var valueFound = _lazyDictionary.GetOrAdd("key", x => new Lazy ( () => { Interlocked.Increment(ref _runCount); Thread.Sleep(100); return valueToPrint; })); Console.WriteLine(valueFound.Value);
此時將得到如下:
我們將第二個參數(shù)修改為Lazy
(1)線程1調(diào)用GetOrAdd方法時,此鍵不存在,此時會調(diào)用valueFactory這個委托。
(2)線程2也調(diào)用GetOrAdd方法,此時線程1還未完成,此時也會調(diào)用valueFactory這個委托。
(3)線程1完成調(diào)用,返回一個未初始化的Lazy
(4)線程2也完成調(diào)用,此時返回一個未初始化的Lazy
(5)線程1調(diào)用Lazy
(6)線程2調(diào)用Lazy
(7)線程3調(diào)用GetOrAdd方法,此時已存在鍵key則不再調(diào)用委托,直接返回鍵key保存的結(jié)果給調(diào)用者。
上述使用Lazy來強(qiáng)迫我們運(yùn)行委托只運(yùn)行一次,如果調(diào)用委托比較耗時此時不利用Lazy來實(shí)現(xiàn)那么將調(diào)用多次,結(jié)果可想而知,現(xiàn)在我們只需要運(yùn)行一次,雖然二者結(jié)果是一樣的。我們通過調(diào)用Lazy
我們接下來看看Lazy對象。方便演示我們定義一個博客類
public class Blog { public string BlogName { get; set; } public Blog() { Console.WriteLine("博客構(gòu)造函數(shù)被調(diào)用"); BlogName = "JeffckyWang"; } }
接下來在控制臺進(jìn)行調(diào)用:
var blog = new Lazy(); Console.WriteLine("博客對象被定義"); if (!blog.IsValueCreated) Console.WriteLine("博客對象還未被初始化"); Console.WriteLine("博客名稱為:" + (blog.Value as Blog).BlogName); if (blog.IsValueCreated) Console.WriteLine("博客對象現(xiàn)在已經(jīng)被初始化完畢");
打印如下:
通過上述打印我們知道當(dāng)調(diào)用blog.Value時,此時博客對象才被創(chuàng)建并返回對象中的屬性字段的值,上述布爾屬性即IsValueCreated顯示表明Lazy對象是否已經(jīng)被初始化,上述初始化對象過程可以簡述如下:
var lazyBlog = new Lazy( () => { var blogObj = new Blog() { BlogName = "JeffckyWang" }; return blogObj; } );
打印結(jié)果和上述一致。上述運(yùn)行都是在非線程安全的模式下進(jìn)行,要是在多線程環(huán)境下對象只被創(chuàng)建一次我們需要用到如下構(gòu)造函數(shù):
public Lazy(LazyThreadSafetyMode mode); public Lazy(FuncvalueFactory, LazyThreadSafetyMode mode);
通過指定LazyThreadSafetyMode的枚舉值來進(jìn)行。
(1)None = 0【線程不安全】
(2)PublicationOnly = 1【針對于多線程,有多個線程運(yùn)行初始化方法時,當(dāng)?shù)谝粋€線程完成時其值則會設(shè)置到其他線程】
(3)ExecutionAndPublication = 2【針對單線程,加鎖機(jī)制,每個初始化方法執(zhí)行完畢,其值則相應(yīng)的輸出】
我們演示下情況:
public class Blog { public int BlogId { get; set; } public Blog() { Console.WriteLine("博客構(gòu)造函數(shù)被調(diào)用"); } }
static void Run(object obj) { var blogLazy = obj as Lazy; var blog = blogLazy.Value as Blog; blog.BlogId++; Thread.Sleep(100); Console.WriteLine("博客Id為:" + blog.BlogId); }
var lazyBlog = new Lazy( () => { var blogObj = new Blog() { BlogId = 100 }; return blogObj; }, LazyThreadSafetyMode.PublicationOnly ); Console.WriteLine("博客對象被定義"); ThreadPool.QueueUserWorkItem(new WaitCallback(Run), lazyBlog); ThreadPool.QueueUserWorkItem(new WaitCallback(Run), lazyBlog);
結(jié)果打印如下:
奇怪的是當(dāng)改變線程安全模式為 LazyThreadSafetyMode.ExecutionAndPublication 時結(jié)果應(yīng)該為101和102才是,居然返回的都是102,但是將上述blog.BogId++和暫停時間順序顛倒時如下:
Thread.Sleep(100); blog.BlogId++;
此時兩個模式返回的都是101和102,不知是何緣故!上述在ConcurrentDictionary類中為了兩個方法能保證線程安全我們利用Lazy來實(shí)現(xiàn),默認(rèn)的模式為 LazyThreadSafetyMode.ExecutionAndPublication 保證委托只執(zhí)行一次。為了不破壞原生調(diào)用ConcurrentDictionary的GetOrAdd方法,但是又為了保證線程安全,我們封裝一個方法來方便進(jìn)行調(diào)用。
public class LazyConcurrentDictionary{ private readonly ConcurrentDictionary > concurrentDictionary; public LazyConcurrentDictionary() { this.concurrentDictionary = new ConcurrentDictionary >(); } public TValue GetOrAdd(TKey key, Func valueFactory) { var lazyResult = this.concurrentDictionary.GetOrAdd(key, k => new Lazy (() => valueFactory(k), LazyThreadSafetyMode.ExecutionAndPublication)); return lazyResult.Value; } }
原封不動的進(jìn)行方法調(diào)用:
_runCount = LazyConcurrentDictionary<, >= LazyConcurrentDictionary<, > Main( task1 = Task.Run(() => PrintValue( task2 = Task.Run(() => PrintValue(.Format( PrintValue( valueFound = _lazyDictionary.GetOrAdd(=>
最終正確打印只運(yùn)行一次的結(jié)果,如下: