c# 死鎖和活鎖的發(fā)生及避免
避免多線程同時(shí)讀寫(xiě)共享數(shù)據(jù)
在實(shí)際開(kāi)發(fā)中,難免會(huì)遇到多線程讀寫(xiě)共享數(shù)據(jù)的需求。比如在某個(gè)業(yè)務(wù)處理時(shí),先獲取共享數(shù)據(jù)(比如是一個(gè)計(jì)數(shù)),再利用共享數(shù)據(jù)進(jìn)行某些計(jì)算和業(yè)務(wù)處理,最后把共享數(shù)據(jù)修改為一個(gè)新的值。由于是多個(gè)線程同時(shí)操作,某個(gè)線程取得共享數(shù)據(jù)后,緊接著共享數(shù)據(jù)可能又被其它線程修改了,那么這個(gè)線程取得的數(shù)據(jù)就是錯(cuò)誤的舊數(shù)據(jù)。我們來(lái)看一個(gè)具體代碼示例:
static int count { get; set; } static void Main(string[] args) { for (int i = 1; i <= 2; i++) { var thread = new Thread(ThreadMethod); thread.Start(i); Thread.Sleep(500); } } static void ThreadMethod(object threadNo) { while (true) { var temp = count; Console.WriteLine("線程 " + threadNo + " 讀取計(jì)數(shù)"); Thread.Sleep(1000); // 模擬耗時(shí)工作 count = temp + 1; Console.WriteLine("線程 " + threadNo + " 已將計(jì)數(shù)增加至: " + count); Thread.Sleep(1000); } }
示例中開(kāi)啟了兩個(gè)獨(dú)立的線程開(kāi)始工作并計(jì)數(shù),假使當(dāng) ThreadMethod
被執(zhí)行第 4 次的時(shí)候(即此刻 count
值應(yīng)為 4),count
值的變化過(guò)程應(yīng)該是:1、2、3、4,而實(shí)際運(yùn)行時(shí)計(jì)數(shù)的的變化卻是:1、1、2、2...。也就是說(shuō),除了第一次,后面每次,兩個(gè)線程讀取到的計(jì)數(shù)都是舊的錯(cuò)誤數(shù)據(jù),這個(gè)錯(cuò)誤數(shù)據(jù)我們把它叫作臟數(shù)據(jù)。
因此,對(duì)共享數(shù)據(jù)進(jìn)行讀寫(xiě)時(shí),應(yīng)視其為獨(dú)占資源,進(jìn)行排它訪問(wèn),避免同時(shí)讀寫(xiě)。在一個(gè)線程對(duì)其進(jìn)行讀寫(xiě)時(shí),其它線程必須等待。避免同時(shí)讀寫(xiě)共享數(shù)據(jù)最簡(jiǎn)單的方法就是加鎖。
修改一下示例,對(duì) count
加鎖:
static int count { get; set; } static readonly object key = new object(); static void Main(string[] args) { ... } static void ThreadMethod(object threadNumber) { while (true) { lock(key) { var temp = count; ... count = temp + 1; ... } Thread.Sleep(1000); } }
這樣就保證了同時(shí)只能有一個(gè)線程對(duì)共享數(shù)據(jù)進(jìn)行讀寫(xiě),避免出現(xiàn)臟數(shù)據(jù)。
死鎖的發(fā)生
上面為了解決多線程同時(shí)讀寫(xiě)共享數(shù)據(jù)問(wèn)題,引入了鎖。但如果同一個(gè)線程需要在一個(gè)任務(wù)內(nèi)占用多個(gè)獨(dú)占資源,這又會(huì)帶來(lái)新的問(wèn)題:死鎖。簡(jiǎn)單來(lái)說(shuō),當(dāng)線程在請(qǐng)求獨(dú)占資源得不到滿足而等待時(shí),又不釋放已占有資源,就會(huì)出現(xiàn)死鎖。死鎖就是多個(gè)線程同時(shí)彼此循環(huán)等待,都等著另一方釋放其占有的資源給自己用,你等我,我待你,你我永遠(yuǎn)都處在彼此等待的狀態(tài),陷入僵局。下面用示例演示死鎖是如何發(fā)生的:
class Program { static void Main(string[] args) { var workers = new Workers(); workers.StartThreads(); var output = workers.GetResult(); Console.WriteLine(output); } } class Workers { Thread thread1, thread2; object resourceA = new object(); object resourceB = new object(); string output; public void StartThreads() { thread1 = new Thread(Thread1DoWork); thread2 = new Thread(Thread2DoWork); thread1.Start(); thread2.Start(); } public string GetResult() { thread1.Join(); thread2.Join(); return output; } public void Thread1DoWork() { lock (resourceA) { Thread.Sleep(100); lock (resourceB) { output += "T1#"; } } } public void Thread2DoWork() { lock (resourceB) { Thread.Sleep(100); lock (resourceA) { output += "T2#"; } } } }
示例運(yùn)行后永遠(yuǎn)沒(méi)有輸出結(jié)果,發(fā)生了死鎖。線程 1 工作時(shí)鎖定了資源 A,期間需要鎖定使用資源 B;但此時(shí)資源 B 被線程 2 獨(dú)占,恰巧資線程 2 此時(shí)又在待資源 A 被釋放;而資源 A 又被線程 1 占用......,如此,雙方陷入了永遠(yuǎn)的循環(huán)等待中。
死鎖的避免
針對(duì)以上出現(xiàn)死鎖的情況,要避免死鎖,可以使用 Monitor.TryEnter(obj, timeout)
方法來(lái)檢查某個(gè)對(duì)象是否被占用。這個(gè)方法嘗試獲取指定對(duì)象的獨(dú)占權(quán)限,如果 timeout
時(shí)間內(nèi)依然不能獲得該對(duì)象的訪問(wèn)權(quán),則主動(dòng)“屈服”,調(diào)用 Thread.Yield()
方法把該線程已占用的其它資源交還給 CUP,這樣其它等待該資源的線程就可以繼續(xù)執(zhí)行了。即,線程在請(qǐng)求獨(dú)占資源得不到滿足時(shí),主動(dòng)作出讓步,避免造成死鎖。
把上面示例代碼的 Workers
類的 Thread1DoWork
方法使用 Monitor.TryEnter
修改一下:
// ...(省略相同代碼) public void Thread1DoWork() { bool mustDoWork = true; while (mustDoWork) { lock (resourceA) { Thread.Sleep(100); if (Monitor.TryEnter(resourceB, 0)) { output += "T1#"; mustDoWork = false; Monitor.Exit(resourceB); } } if (mustDoWork) Thread.Yield(); } } public void Thread2DoWork() { lock (resourceB) { Thread.Sleep(100); lock (resourceA) { output += "T2#"; } } }
再次運(yùn)行示例,程序正常輸出 T2#T1#
并正常結(jié)束,解決了死鎖問(wèn)題。
注意,這個(gè)解決方法依賴于線程 2 對(duì)其所需的獨(dú)占資源的固執(zhí)占有和線程 1 愿意“屈服”作出讓步,讓線程 2 總是優(yōu)先執(zhí)行。同時(shí)注意,線程 1 在鎖定 resourceA
后,由于爭(zhēng)奪不到 resourceB
,作出了讓步,把已占有的 resourceA
釋放掉后,就必須等線程 2 使用完 resourceA
重新鎖定 resourceA
再重做工作。
正因?yàn)榫€程 2 總是優(yōu)先,所以,如果線程 2 占用 resourceA
或 resourceB
的頻率非常高(比如外面再嵌套一個(gè)類似 while(true) 的循環(huán) ),那么就可能導(dǎo)致線程 1 一直無(wú)法獲得所需要的資源,這種現(xiàn)象叫線程饑餓,是由高優(yōu)先級(jí)線程吞噬低優(yōu)先級(jí)線程 CPU 執(zhí)行時(shí)間的原因造成的。線程饑餓除了這種的原因,還有可能是線程在等待一個(gè)本身也處于永久等待完成的任務(wù)。
我們可以繼續(xù)開(kāi)個(gè)腦洞,上面示例中,如果線程 2 也愿意讓步,會(huì)出現(xiàn)什么情況呢?
活鎖的發(fā)生和避免
我們把上面示例改造一下,使線程 2 也愿意讓步:
public void Thread1DoWork() { bool mustDoWork = true; Thread.Sleep(100); while (mustDoWork) { lock (resourceA) { Console.WriteLine("T1 重做"); Thread.Sleep(1000); if (Monitor.TryEnter(resourceB, 0)) { output += "T1#"; mustDoWork = false; Monitor.Exit(resourceB); } } if (mustDoWork) Thread.Yield(); } } public void Thread2DoWork() { bool mustDoWork = true; Thread.Sleep(100); while (mustDoWork) { lock (resourceB) { Console.WriteLine("T2 重做"); Thread.Sleep(1100); if (Monitor.TryEnter(resourceA, 0)) { output += "T2#"; mustDoWork = false; Monitor.Exit(resourceB); } } if (mustDoWork) Thread.Yield(); } }
注意,為了使我要演示的效果更明顯,我把兩個(gè)線程的 Thread.Sleep 時(shí)間拉開(kāi)了一點(diǎn)點(diǎn)。運(yùn)行后的效果如下:
通過(guò)觀察運(yùn)行效果,我們發(fā)現(xiàn)線程 1 和線程 2 一直在相互讓步,然后不斷重新開(kāi)始。兩個(gè)線程都無(wú)法進(jìn)入 Monitor.TryEnter
代碼塊,雖然都在運(yùn)行,但卻沒(méi)有真正地干活。
我們把這種線程一直處于運(yùn)行狀態(tài)但其任務(wù)卻一直無(wú)法進(jìn)展的現(xiàn)象稱為活鎖?;铈i和死鎖的區(qū)別在于,處于活鎖的線程是運(yùn)行狀態(tài),而處于死鎖的線程表現(xiàn)為等待;活鎖有可能自行解開(kāi),死鎖則不能。
要避免活鎖,就要合理預(yù)估各線程對(duì)獨(dú)占資源的占用時(shí)間,并合理安排任務(wù)調(diào)用時(shí)間間隔,要格外小心?,F(xiàn)實(shí)中,這種業(yè)務(wù)場(chǎng)景很少見(jiàn)。示例中這種復(fù)雜的資源占用邏輯,很容易把人搞蒙,而且極不容易維護(hù)。推薦的做法是使用信號(hào)量機(jī)制代替鎖,這是另外一個(gè)話題,后面單獨(dú)寫(xiě)文章講。
總結(jié)
我們應(yīng)該避免多線程同時(shí)讀寫(xiě)共享數(shù)據(jù),避免的方式,最簡(jiǎn)單的就是加鎖,把共享數(shù)據(jù)作為獨(dú)占資源來(lái)進(jìn)行排它使用。
多個(gè)線程在一次任務(wù)中需要對(duì)多個(gè)獨(dú)占資源加鎖時(shí),就可能因相互循環(huán)等待而出現(xiàn)死鎖。要避免死鎖,就至少得有一個(gè)線程作出讓步。即,在發(fā)現(xiàn)自己需要的資源得不到滿足時(shí),就要主動(dòng)釋放已占有的資源,以讓別的線程可以順利執(zhí)行完成。
大部分情況安排一個(gè)線程讓步便可避免死鎖,但在復(fù)雜業(yè)務(wù)中可能會(huì)有多個(gè)線程互相讓步的情況造成活鎖。為了避免活鎖,需要合理安排線程任務(wù)調(diào)用的時(shí)間間隔,而這會(huì)使得業(yè)務(wù)代碼變得非常復(fù)雜。更好的做法是放棄使用鎖,而換成使用信號(hào)量機(jī)制來(lái)實(shí)現(xiàn)對(duì)資源的獨(dú)占訪問(wèn)。
作者:精致碼農(nóng)
以上就是c# 死鎖和活鎖的發(fā)生及避免的詳細(xì)內(nèi)容,更多關(guān)于c# 死鎖和活鎖的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解C#讀取Appconfig中自定義的節(jié)點(diǎn)
我們往往需要在App.config中自定義一些節(jié)來(lái)滿足實(shí)際需要,而不依賴于App.config的appSettings,下面通過(guò)一個(gè)簡(jiǎn)單的實(shí)例來(lái)說(shuō)明自定義配置節(jié)點(diǎn)的設(shè)置與讀取2015-06-06C#在驗(yàn)證文件共享模式下實(shí)現(xiàn)多線程文件寫(xiě)入
這篇文章主要為大家詳細(xì)介紹了C#在驗(yàn)證文件共享模式下實(shí)現(xiàn)多線程文件寫(xiě)入的相關(guān)知識(shí),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解下2024-01-01基于C#實(shí)現(xiàn)的多邊形沖突檢測(cè)實(shí)例
這篇文章主要給大家介紹了基于C#實(shí)現(xiàn)的多邊形沖突檢測(cè)的相關(guān)資料,文中介紹的方法并未使用第三方類庫(kù),可以完美解決這個(gè)問(wèn)題,需要的朋友可以參考下2021-07-07C#實(shí)現(xiàn)多種圖片格式轉(zhuǎn)換的示例詳解
這篇文章主要為大家詳細(xì)介紹了C#如何實(shí)現(xiàn)多種圖片格式轉(zhuǎn)換,例如轉(zhuǎn)換成圖標(biāo)圖像ICO,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-01-01C# 實(shí)現(xiàn)特殊字符快速轉(zhuǎn)碼
這篇文章主要介紹了C# 實(shí)現(xiàn)特殊字符快速轉(zhuǎn)碼,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-01-01基于C#后臺(tái)調(diào)用跨域MVC服務(wù)及帶Cookie驗(yàn)證的實(shí)現(xiàn)
本篇文章介紹了,基于C#后臺(tái)調(diào)用跨域MVC服務(wù)及帶Cookie驗(yàn)證的實(shí)現(xiàn)。需要的朋友參考下2013-04-04C#實(shí)現(xiàn)讀取和設(shè)置文件與文件夾的權(quán)限
這篇文章主要為大家詳細(xì)介紹了如何使用C#實(shí)現(xiàn)讀取和設(shè)置文件與文件夾的權(quán)限,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-03-03c#調(diào)用c++的DLL的實(shí)現(xiàn)方法
本文主要介紹了c#調(diào)用c++的DLL的實(shí)現(xiàn)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05