欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

c# 死鎖和活鎖的發(fā)生及避免

 更新時(shí)間:2020年11月13日 11:11:28   作者:精致碼農(nóng) • 王亮  
多線程編程時(shí),如果涉及同時(shí)讀寫(xiě)共享數(shù)據(jù),就要格外小心。如果共享數(shù)據(jù)是獨(dú)占資源,則要對(duì)共享數(shù)據(jù)的讀寫(xiě)進(jìn)行排它訪問(wèn),最簡(jiǎn)單的方式就是加鎖。鎖也不能隨便用,否則可能會(huì)造成死鎖和活鎖。本文將通過(guò)示例詳細(xì)講解死鎖和活鎖是如何發(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 占用 resourceAresourceB 的頻率非常高(比如外面再嵌套一個(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)

出處:http://cnblogs.com/willick

以上就是c# 死鎖和活鎖的發(fā)生及避免的詳細(xì)內(nèi)容,更多關(guān)于c# 死鎖和活鎖的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評(píng)論