亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

c#中多線程間的同步示例詳解

 更新時(shí)間:2021年09月30日 14:19:14   作者:化云隨風(fēng)  
使用線程時(shí)最頭痛的就是共享資源的同步問(wèn)題,處理不好會(huì)得到錯(cuò)誤的結(jié)果,所以下面這篇文章主要給大家介紹了關(guān)于c#中多線程間同步的相關(guān)資料,需要的朋友可以參考下

一、引入

先給出一個(gè)Num類的定義

internal class Num
{
  public static int odd = 50000;
  public static int even = 10000;   
}

假設(shè)現(xiàn)在要求輸出小于 odd 的所有奇數(shù),輸出小于 even 的所有偶數(shù),不考慮多線程時(shí)可以寫出如下的代碼:(為了演示多線程時(shí)線程間的爭(zhēng)用,先把值賦給了 num,實(shí)際上這個(gè)賦值操作毫無(wú)意義 )

//同步代碼段
public long Sum()
	{
  	Stopwatch sw = new Stopwatch();
    sw.Start();
    int num = 0;
    for (int i = 0; i <= Num.odd; i++)
    {
         num = i; 
         if ((i & 1) == 1)
         {
            Console.WriteLine($"奇數(shù):{num}");
         }
     }           

    for (int i = 0; i <= Num.even; i++)
    {
        num = i;
        if ((i & 1) == 0)
        {                    
            Thread.Sleep(10);  
            Console.WriteLine($"偶數(shù):{num}");
        }
    }
  
  	sw.Stop();
    return sw.ElapsedMilliseconds;
}

現(xiàn)在,因?yàn)楹臅r(shí)太長(zhǎng),引入多線程進(jìn)行處理,修改為如下形式:

//NoLock Task 
  private readonly object sync = new();
  int num = 0; 
  public int Sum()
  {
    Stopwatch sw = new Stopwatch();
    sw.Start();
    var ta =Task.Run(() =>
    {               
      for (int i = 0; i <= Num.odd; i++)
      {
        num = i; //判斷條件之前賦值是為了提高觸發(fā)幾率
        if((i & 1) == 1)
        {
        	Console.WriteLine($"奇數(shù):{num}");
        }                                        
      }
    });

    var tb =  Task.Run(() =>
    {              
    	for (int i = 0; i <= Num.even; i++)
      {
        num = i;
        if ((i & 1) == 0)
        {
        	Thread.Sleep(10);  //在此處添加延時(shí),在 tb 線程等待時(shí),num.sum的值可能已經(jīng)被 ta 修改為了其他值
        	Console.WriteLine($"偶數(shù):{num}");
        }
      }
    });

    Task.WaitAll(ta, tb);  //為了保證任務(wù)完成,獲取執(zhí)行時(shí)間
    sw.Stop();
    return sw.ElapsedMilliseconds;
  }              
}

上面這段代碼中,我們期望線程ta會(huì)輸出odd以內(nèi)的奇數(shù)值,線程tb會(huì)輸出even以內(nèi)的偶數(shù)。但實(shí)際運(yùn)行時(shí)會(huì)出現(xiàn)下圖所示的情況。

如上,當(dāng)程序涉及到多線程的時(shí)候,在 [各線程間共享的數(shù)據(jù)] 總會(huì)因?yàn)榫€程間的爭(zhēng)用導(dǎo)致意料之外的情況,為此,各種語(yǔ)言也會(huì)提供協(xié)助線程間同步的特性,這里簡(jiǎn)單記錄一下我對(duì)c#中機(jī)制的理解。

二、Lock

借用Lock的方法對(duì)示例進(jìn)行修改,可以有兩種方式,此處展示ta部分,tb 與ta做相同修改:

//方式一:在Task內(nèi)全局Lock
var ta = Task.Run(() =>
  {
    	lock (sync)
    	{	
        for (int i = 0; i <= 10000; i++)
        {                       
          num = i; //判斷條件之前賦值是為了提高觸發(fā)幾率
          if ((i & 1) == 1)
          {
            Console.WriteLine($"奇數(shù):{num}");
          }                                              
        }
    	}
   });

//方式二: 為每一次For循環(huán)Lock
var ta = Task.Run(() =>
{
  //lock (sync)  
    for (int i = 0; i <= Num.odd; i++)
    {
      lock(sync)
      {
        num = i; //判斷條件之前賦值是為了提高觸發(fā)幾率
        if ((i & 1) == 1)
        {
       	  Console.WriteLine($"奇數(shù):{num}");
        }
      }    
  }
});

上述的兩種方式中,

  • 方法一相當(dāng)于對(duì)Task進(jìn)行了鎖定,同一時(shí)刻只能運(yùn)行一個(gè)被鎖定的代碼段(即取決于當(dāng)前對(duì)象實(shí)例中ta,tb誰(shuí)先取得了使用權(quán)),這樣多線程其實(shí)退化為了單線程處理。
  • 方法二應(yīng)該是更合理的使用方式,每一次循環(huán)時(shí)進(jìn)行鎖定,保證了每次賦值及使用時(shí)的獨(dú)占性,也不影響另一個(gè)線程的循環(huán)操作。方式二仍然會(huì)存在一個(gè)線程等待的情況,只是會(huì)比第一種方式好一些。但是,對(duì)每一次循環(huán)進(jìn)行Lock,性能是需要考慮的一個(gè)點(diǎn)。

說(shuō)回Lock本身,網(wǎng)上有很多文章介紹,lock只是一個(gè)語(yǔ)法糖,編譯器會(huì)將其轉(zhuǎn)換為對(duì) monitor 的調(diào)用。

IL代碼如下圖:

可以看到,編譯器會(huì)幫我們構(gòu)建try塊,并在finally塊調(diào)用Monitor.Exit方法。若要獲取更精細(xì)的控制,可以自己調(diào)用Monitor進(jìn)行使用。

三、Monitor

monitor與lock相比更為靈活,可以使用IsEntered(object)判斷當(dāng)前線程是否獲取到了sync的鎖定,可以使用 TryEnter()嘗試獲取排它鎖,也可以調(diào)用重載方法指定等待時(shí)間。

需要指出的是,

1、所有等待獲取鎖定的線程會(huì)處于阻塞狀態(tài)。

2、在等待獲取鎖定的線程上執(zhí)行Thread.Interrupt會(huì)中斷當(dāng)前線程的等待并拋出ThreadInterruptedException的異常

3、monitor與lock鎖定的對(duì)象sync必須為引用類型,查看反編譯的代碼會(huì)發(fā)現(xiàn)在每一次lock之前,會(huì)將sync賦值給一個(gè)object對(duì)象,如果sync為值類型,則會(huì)被裝箱為一個(gè)新的對(duì)象。

4、以鎖定For循環(huán)為例,在定義鎖定對(duì)象時(shí),可以定義為

   public class LockFor
   {
       private readonly object sync = new();
       ....
   }

或者定義為

 public class LockForStaticSync
 {
        private static readonly object sync = new();
   			...
 }

后者添加一個(gè) static 修飾符將其定義靜態(tài)只讀對(duì)象。

我們知道,static 修飾的變量并不屬于對(duì)象,而是歸屬于 class 本身,按照我的理解來(lái)說(shuō),如果在多個(gè)線程中都實(shí)例化了 LockForStaticSync 的對(duì)象,并同時(shí)調(diào)用一個(gè) SUM 方法時(shí),不論線程間是否會(huì)發(fā)生沖突,只能有一個(gè)線程取得sync的鎖定繼續(xù)執(zhí)行,其他的線程會(huì)處于阻塞等待狀態(tài)。

而如果是第一種定義方式,則多個(gè)線程的多個(gè)對(duì)象間,不會(huì)產(chǎn)生沖突,所有線程都可以執(zhí)行。

此例的場(chǎng)景下,多實(shí)例調(diào)用時(shí),方式一應(yīng)該會(huì)有速度上的絕對(duì)優(yōu)勢(shì)。為此我刪除了任務(wù)內(nèi)打印及等待的部分,執(zhí)行了如下內(nèi)容進(jìn)行驗(yàn)證:

private static void Invocation()
{
  Stopwatch sw = new Stopwatch();
  sw.Start();
  var t1 = new TaskFactory().StartNew(() => new LockFor().Sum());
  var t2 = new TaskFactory().StartNew(() => new LockFor().Sum());
  var t3 = new TaskFactory().StartNew(() => new LockFor().Sum());
  Task.WaitAll(t1, t2, t3);
  sw.Stop();
  Console.WriteLine(sw.ElapsedMilliseconds);

  sw.Restart();
  var t4 = new TaskFactory().StartNew(() => new LockForStaticSync().Sum());
  var t5 = new TaskFactory().StartNew(() => new LockForStaticSync().Sum());
  var t6 = new TaskFactory().StartNew(() => new LockForStaticSync().Sum());
  Task.WaitAll(t4, t5, t6);
  sw.Stop();
  Console.WriteLine(sw.ElapsedMilliseconds);
}

但實(shí)際執(zhí)行結(jié)果是出乎意料的,多次運(yùn)行后,類變量鎖的運(yùn)行速度遠(yuǎn)超對(duì)象變量的方式,這是為什么呢?

思考之后,考慮到類變量省略了每次創(chuàng)建鎖定對(duì)象的時(shí)間,而數(shù)量較少的循環(huán)次數(shù)可能無(wú)法彌補(bǔ)這個(gè)時(shí)間差,于是我逐漸調(diào)大Num類中定義的變量。隨著循環(huán)次數(shù)的增加,也就是方法執(zhí)行時(shí)間的增加,對(duì)象變量的優(yōu)勢(shì)逐漸顯現(xiàn)

既然對(duì)象變量有速度上的優(yōu)勢(shì),而使用過(guò)程中又不可避免的會(huì)出現(xiàn)多方調(diào)用的情況,那么是不是應(yīng)該一直選擇定義為對(duì)象變量呢?其實(shí)不然,比如靜態(tài)類中需要的鎖定對(duì)象,全局緩存字典,文件操作幫助類都應(yīng)該是靜態(tài)鎖,更多場(chǎng)景,歡迎補(bǔ)充。

四、Interlocked

Interlocked類中的方法可以實(shí)現(xiàn)原子操作,通過(guò)操作系統(tǒng)及硬件CPU級(jí)別的控制,確保CPU在執(zhí)行當(dāng)前操作時(shí)不會(huì)被中斷,這個(gè)類里面提供了一些簡(jiǎn)單的方法,如Add,Increment等。

五、Semaphore

信號(hào)量是一種計(jì)數(shù)的互斥鎖定。什么意思呢?以Monitor來(lái)講,從monitor.enetr開(kāi)始,到monitor.exit為止,被包裹著的這一段代碼,同一時(shí)刻只能由一個(gè)線程訪問(wèn),而 Semaphore 可以定義同時(shí)訪問(wèn)某些資源的線程數(shù)量,即允許多線程同時(shí)訪問(wèn)被保護(hù)的代碼。

Semaphore有三種簽名的構(gòu)造函數(shù),其中 Semaphore(int initialCount, int maximumCount) 的參數(shù)指定最初釋放的信號(hào)量可用數(shù)量與最大量,兩者的差值歸創(chuàng)建信號(hào)量的線程所有。

以下內(nèi)容來(lái)自 MSDN

class SemaphoreTest
    {       
        // A semaphore that simulates a limited resource pool.     
        private static Semaphore _pool;

        // 協(xié)助設(shè)置線程休眠時(shí)間.
        private static int _padding;

        public static void Main()
        {
            // 這里是創(chuàng)建了最大可以有三個(gè)訪問(wèn)線程的信號(hào)量,但此時(shí)可用為0,需要當(dāng)前線程釋放以后才可用。
            // 如果此處設(shè)置可用量非0,主線程釋放時(shí)仍然傳遞了3,程序運(yùn)行過(guò)程中會(huì)出現(xiàn)SemaphoreFullException的異常
            _pool = new Semaphore(0, 3);                       
            for (int i = 1; i <= 5; i++)
            {
                Thread t = new Thread(new ParameterizedThreadStart(Worker));                           
                t.Start(i);
            }

            // 主線程休眠,讓其他線程運(yùn)行到等待信號(hào)量的狀態(tài)            
            Thread.Sleep(500);

            //主線程調(diào)用Release(3),將可用量設(shè)置為最大值,正在等待的線程會(huì)獲得信號(hào)                       
            Console.WriteLine("Main thread calls Release(3)."); 
            _pool.Release(3);

            Console.WriteLine("Main thread exits.");
            Console.ReadLine();
        }

        private static void Worker(object num)
        {
            // 阻塞當(dāng)前線程,直到獲取到信號(hào)量         
            Console.WriteLine("Thread {0} begins " +"and waits for the semaphore.", num);
            _pool.WaitOne(); 

            // 每一個(gè)線程等待的時(shí)間間隔參數(shù)
            int padding = Interlocked.Add(ref _padding, 100);
            Console.WriteLine("Thread {0} enters the semaphore.", num);
            
            //與padding共用,讓輸出更有順序
            Thread.Sleep(1000 + padding);

            Console.WriteLine("Thread {0} releases the semaphore.", num);
            Console.WriteLine("Thread {0} previous semaphore count: {1}",num, _pool.Release());                    }
    }

目前為止,提到的內(nèi)容均是在同一個(gè)進(jìn)程內(nèi)同步的方法,信號(hào)量作為一個(gè)系統(tǒng)級(jí)的存在,是可以幫助我們實(shí)現(xiàn)進(jìn)程間同步的,只需要在創(chuàng)建 Seamphore 對(duì)象的實(shí)例時(shí),為信號(hào)量指定名稱即可。

另外特別需要注意的是,信號(hào)量是可重入的,簡(jiǎn)單說(shuō)就是可以在一個(gè)線程內(nèi)執(zhí)行多次 WaitOne() 方法,多次調(diào)用時(shí),如果處理不好,就可能出現(xiàn)意外情況,修改 Worker 為下方內(nèi)容

private static void Worker(object num)
        {            
            Console.WriteLine("Thread {0} begins " +
                "and waits for the semaphore.", num);
            _pool.WaitOne();
            Console.WriteLine($"{num} getOne");
            // A padding interval to make the output more orderly.
            int padding = Interlocked.Add(ref _padding, 100);
            _pool.WaitOne();     //此處添加一處調(diào)用
            Console.WriteLine("Thread {0} enters the semaphore.", num);          
            
            Thread.Sleep(1000 + padding);
						
            Console.WriteLine("Thread {0} releases the semaphore.", num);
            Console.WriteLine("Thread {0} previous semaphore count: {1}",
                num, _pool.Release());
					//這里應(yīng)該再調(diào)用一次_pool.Release(),或者在上一句傳入?yún)?shù),改為 _pool.Release(2)
        }

建議實(shí)際運(yùn)行一下,查看運(yùn)行的表現(xiàn),運(yùn)行過(guò)后會(huì)發(fā)現(xiàn),任務(wù)線程全部阻塞在了第二處 WaitOne,導(dǎo)致程序無(wú)法向下執(zhí)行。

因此,重入必須要謹(jǐn)慎,而且退出時(shí)必須要釋放等量的鎖定數(shù)值,如果執(zhí)行了多余的Release(),最終程序在運(yùn)行過(guò)程中會(huì)出現(xiàn)

SemaphoreFullException的異常

六、Event

與信號(hào)量一樣,事件也是一個(gè)系統(tǒng)范圍內(nèi)的資源同步方法。又分為ManualResetEvent,AutoResetEvent,CountdownEvent以及ManualResetEventSlim,在構(gòu)建對(duì)象實(shí)例時(shí)若傳入了name參數(shù),代表這是一個(gè)可以跨進(jìn)程的系統(tǒng)級(jí)同步事件。

ManualResetEvent 為例,該類有 signaled 和 nonsignaled 兩種狀態(tài),這兩種狀態(tài)通過(guò)實(shí)例化對(duì)象時(shí)的 布爾類型 參數(shù)決定,TRUE 就是signaled, False相反

文檔中常見(jiàn)的翻譯是發(fā)出信號(hào)的狀態(tài)和未發(fā)出信號(hào)的狀態(tài),微軟官網(wǎng)的機(jī)翻是終止?fàn)顟B(tài)和非終止?fàn)顟B(tài),還有一些釋放線程之類的描述,直觀上難以理解。其實(shí)就是改變狀態(tài)而已,還不如英文的好理解。

ManualResetEvent 的基類 EventWaitHandle 中提供了Set() 和Reset() 方法,用于改變狀態(tài),Set 將事件修改為 signaled,Reset重置為 nonsignaled

這里說(shuō)一下Set和Reset,這兩個(gè)方法是改變了事件的狀態(tài),并不是一個(gè)瞬時(shí)性的動(dòng)作,也就意味著在調(diào)用Set后,調(diào)用Reset之前,事件都處于 signaled 狀態(tài)(AutoResetEvent會(huì)自動(dòng)調(diào)用Reset重置事件狀態(tài))

WaitHandle 類中提供了眾多等待信號(hào)的方法,EventWaitHandle 繼承自WaitHandle, ManualResetEvent中也可以調(diào)用WaitOne等方法。

回頭再看一下信號(hào)量 Semaphore 的示例,也調(diào)用 WaitOne 等待信號(hào),因?yàn)樗怖^承自 Waithandler。

有了上面的基礎(chǔ),可以看一下下面 MSDN 的示例

private static ManualResetEvent mre = new ManualResetEvent(false);
static void Main()
{           
  for (int i = 0; i <= 2; i++)
  {
    Thread t = new Thread(ThreadProc);
    t.Name = "Thread_" + i;
    t.Start();
  }

  Thread.Sleep(500);
  Console.WriteLine(@"線程012都會(huì)處于等待狀態(tài),直至 mre修改狀態(tài),按 Enter繼續(xù)");
  Console.ReadLine();

  mre.Set();

  Thread.Sleep(500);
  Console.WriteLine(@"調(diào)用了mre.set后,事件處于 signaled 狀態(tài),下一個(gè)線程不會(huì)被阻塞");
  Console.ReadLine();

  for (int i = 3; i <= 4; i++)
  {
    Thread t = new Thread(ThreadProc);
    t.Name = "Thread_" + i;
    t.Start();
  }

  Thread.Sleep(500);
  Console.WriteLine("調(diào)用mre.reset后,事件處于nonsignaled狀態(tài),此時(shí)會(huì)被阻塞");
  Console.ReadLine();
  mre.Reset();

  Thread t5 = new Thread(ThreadProc);
  t5.Name = "Thread_5";
  t5.Start();

  Thread.Sleep(500);
  Console.WriteLine("\nPress Enter to call Set() and conclude the demo.");
  Console.ReadLine();

  mre.Set();
  Console.ReadLine();
}

private static void ThreadProc()
{
  string name = Thread.CurrentThread.Name;
  Console.WriteLine(name + " starts and calls mre.WaitOne()");
  mre.WaitOne();
  Console.WriteLine(name + " ends.");
}
  • AutoResetEvent,名字可以看出來(lái)這個(gè)類會(huì)自動(dòng)調(diào)用 Reset方法,事實(shí)也是這樣,這個(gè)類會(huì)在等待線程執(zhí)行結(jié)束后將事件置為non-signaled
  • ManualResetEventSlim 是 ManualResetEvent的輕量實(shí)現(xiàn),他并不是繼承自 EventWaitHandle基類,
  • CountdownEvent 也不是繼承自 EventWaitHandle基類,它會(huì)在初始化時(shí)得到一個(gè)數(shù)值,稱為InitialCount,同時(shí)賦給CurrentCount , 每次調(diào)用Signal時(shí),CurrentCount會(huì)減少相應(yīng)的值,當(dāng)調(diào)用后CurrentCount為0時(shí),會(huì)發(fā)出信號(hào),并將其設(shè)置為 IS_SET 狀態(tài)。

七、Barrier

Barrier 是一個(gè)有意思的類,可以使多個(gè)任務(wù)能夠采用并行方式依據(jù)某種算法在多個(gè)階段中協(xié)同工作。

Enables multiple tasks to cooperatively work on an algorithm in parallel through multiple phases.

大白話來(lái)講,借助于這個(gè)對(duì)象,可以管控多個(gè)并行任務(wù)間的合作關(guān)系。

Barrier的構(gòu)造函數(shù)中可以傳入并行任務(wù)的數(shù)量(participantCount),也可以通過(guò) AddParticipant,RemoveParticipant 動(dòng)態(tài)地調(diào)整參與者的數(shù)量。另外可以通過(guò) CurrentPhaseNumber 得知當(dāng)前是第幾個(gè)參與者,通過(guò) ParticipantsRemaining 知道還有幾個(gè)參與者未到達(dá)任務(wù)點(diǎn)。

還可以傳入一個(gè)可空委托,這個(gè)委托會(huì)在接收到所有參與者線程發(fā)出信號(hào)后執(zhí)行。

就好像幾個(gè)人一起玩游戲闖關(guān),關(guān)卡boss需要所有人一起才能打敗,照顧到每個(gè)人的游戲理解不同,規(guī)定每個(gè)人可以按照自己的安排推進(jìn)游戲進(jìn)度。

在這個(gè)比喻中,每個(gè)人都是單獨(dú)的線程,可能有人很快就抵達(dá)了關(guān)卡,但是因?yàn)殛P(guān)卡的性質(zhì),他必須在這里等待其他人都到達(dá)后,大家一起打boss,打敗boss之后存檔,之后大家再各玩各的,直到下一個(gè)關(guān)卡BOSS。

Barrier就承擔(dān)了boss的任務(wù),他負(fù)責(zé)讓所有線程抵達(dá)某一個(gè)預(yù)設(shè)的點(diǎn)后再一起放行。放行之后,會(huì)執(zhí)行初始化對(duì)象時(shí)傳入的委托,比如上方說(shuō)的存檔。只是我們可以通過(guò)委托,更靈活地指定要進(jìn)行的操作。

而所謂的預(yù)設(shè)點(diǎn),其實(shí)就是調(diào)用 SignalAndWait,發(fā)出信號(hào)并等待其他線程發(fā)出信號(hào)的代碼

如果你暫時(shí)沒(méi)有理解我上面的比喻,那就看一下下面的代碼吧,代碼是在MSDN拿來(lái)的,我自己加了幾個(gè)console語(yǔ)句,可以運(yùn)行看看效果

public static void Barriers()
        {
            int count = 0;
           //初始化 3 個(gè)參與者,傳入委托
            Barrier barrier = new Barrier(3, (b) =>
            {
                Console.WriteLine("Post-Phase action: count={0}, phase={1},threadid={2}", count, b.CurrentPhaseNumber,Thread.CurrentThread.ManagedThreadId);
                if (b.CurrentPhaseNumber == 2)             
                    throw new Exception("D'oh!");
            });

            barrier.AddParticipants(2);

            barrier.RemoveParticipant(); //剩下4個(gè)
            Console.WriteLine($"主Thread:{Thread.CurrentThread.ManagedThreadId}");

            // This is the logic run by all participants
            Action action = () =>
            {
                Console.WriteLine($"action1 Thread:{Thread.CurrentThread.ManagedThreadId}");
                Interlocked.Increment(ref count);
                barrier.SignalAndWait(); // during the post-phase action, count should be 4 and phase should be 0

                Console.WriteLine($"action2 Thread:{Thread.CurrentThread.ManagedThreadId}");
                Interlocked.Increment(ref count);
                barrier.SignalAndWait(); // during the post-phase action, count should be 8 and phase should be 1
                Console.WriteLine($"action3 Thread:{Thread.CurrentThread.ManagedThreadId}");

                // 線程3會(huì)引發(fā)委托里拋出的異常,異常信息所有線程可見(jiàn)
                Interlocked.Increment(ref count);
                try
                {
                    barrier.SignalAndWait();
                }
                catch (BarrierPostPhaseException bppe)
                {
                    Console.WriteLine("Caught BarrierPostPhaseException: {0}", bppe.Message);
                }
                Console.WriteLine($"action4 Thread:{Thread.CurrentThread.ManagedThreadId}");

                // The fourth time should be hunky-dory
                Interlocked.Increment(ref count);
                barrier.SignalAndWait(); // during the post-phase action, count should be 16 and phase should be 3
                Console.WriteLine($"action5 Thread:{Thread.CurrentThread.ManagedThreadId}");

            };

            // 啟動(dòng)與 Barrier設(shè)置數(shù)量相同的任務(wù),如果啟動(dòng)數(shù)目超過(guò)設(shè)置值,會(huì)引發(fā)如下異常
  				  //"System.InvalidOperationException: The number of threads using the barrier exceeded the total number of registered participants."             
            Parallel.Invoke(action, action, action, action);
                       
            Console.WriteLine($"主Thread2:{Thread.CurrentThread.ManagedThreadId}");

            // It's good form to Dispose() a barrier when you're done with it.
            barrier.Dispose();
        }

執(zhí)行效果如下:

可以在24行,27行打斷點(diǎn),借助上面的比喻,理解一下Barrier的用法。

最后要聲明的是Barrier的public protected 成員是線程安全的,可以跨線程使用。但是Dispose是非線程安全的,意味著一旦調(diào)用,所有線程都會(huì)受到影響,應(yīng)該在任務(wù)代碼之外執(zhí)行,另外既然有dispose的方法,就要注意使用完畢后調(diào)用該方法釋放資源。

八、ReaderWriterLockSlim

讀寫鎖,允許多個(gè)線程處于讀取模式,允許一個(gè)線程處于具有獨(dú)占鎖定權(quán)限的寫入模式,并且允許具有讀取訪問(wèn)權(quán)限的一個(gè)線程處于可升級(jí)讀取模式,在該模式下,線程可以升級(jí)到寫入模式,而無(wú)需放棄對(duì)資源的讀取訪問(wèn)權(quán)限。

讀寫鎖具有三種模式,讀鎖,寫鎖,可升級(jí)的讀鎖

讀鎖可以通過(guò) EnterReadLock 進(jìn)入,通過(guò) ExitReadLock 退出,寫鎖類似EnterWriteLock,ExitWriteLock

可升級(jí)的讀鎖是指可以直接由讀轉(zhuǎn)換為寫模式的狀態(tài),EnterUpgradeableReadLock及ExitUpgradeableReadLock

與其他同步對(duì)象相同,讀寫鎖需要正確的進(jìn)行釋放,不然會(huì)引發(fā)問(wèn)題

讀寫鎖初始化時(shí),可以傳入 LockRecursionPolicy 指定遞歸狀態(tài),默認(rèn)的構(gòu)造函數(shù)為NoRecursion,微軟官網(wǎng)并不建議新手使用遞歸策略,因?yàn)檫@具有更高的復(fù)雜性,而且容易帶來(lái)死鎖的問(wèn)題,我自己也沒(méi)有用過(guò)遞歸策略。

與ReaderWriterLock相比,ReaderWriterLockSlim是被推薦使用的對(duì)象

對(duì)于 ReaderWriterLockSlim 的使用,我在園子里有個(gè)提問(wèn)請(qǐng)教ReaderWriterLockSlim的問(wèn)題,建議轉(zhuǎn)過(guò)去看下,我這邊就不放代碼了,當(dāng)時(shí)找到了另一篇文章解答了我的疑惑,地址也貼在這里 C# ReaderWriterLockSlim 實(shí)現(xiàn) - dz45693.

這里面就有一些需要注意的點(diǎn),這幾個(gè)點(diǎn)全部是摘抄自上面那篇文章,請(qǐng)知悉:

  • 對(duì)于同一把鎖、多個(gè)線程可同時(shí)進(jìn)入讀模式。
  • 對(duì)于同一把鎖、同時(shí)只允許一個(gè)線程進(jìn)入寫模式。
  • 對(duì)于同一把鎖、同時(shí)只允許一個(gè)線程進(jìn)入可升級(jí)的讀模式。
  • 通過(guò)默認(rèn)構(gòu)造函數(shù)創(chuàng)建的讀寫鎖是不支持遞歸的,若想支持遞歸 可通過(guò)構(gòu)造 ReaderWriterLockSlim(LockRecursionPolicy) 創(chuàng)建實(shí)例。
  • 對(duì)于同一把鎖、同一線程不可兩次進(jìn)入同一鎖狀態(tài)(開(kāi)啟遞歸后可以)
  • 對(duì)于同一把鎖、即便開(kāi)啟了遞歸、也不可以在進(jìn)入讀模式后再次進(jìn)入寫模式或者可升級(jí)的讀模式(在這之前必須退出讀模式)。
  • 再次強(qiáng)調(diào)、不建議啟用遞歸。
  • 讀寫鎖具有線程關(guān)聯(lián)性,即兩個(gè)線程間擁有的鎖的狀態(tài)相互獨(dú)立不受影響、并且不能相互修改其鎖的狀態(tài)。
  • 升級(jí)狀態(tài):在進(jìn)入可升級(jí)的讀模式 EnterUpgradeableReadLock后,可在恰當(dāng)時(shí)間點(diǎn)通過(guò)EnterWriteLock進(jìn)入寫模式。
  • 降級(jí)狀態(tài):可升級(jí)的讀模式可以降級(jí)為讀模式:即在進(jìn)入可升級(jí)的讀模式EnterUpgradeableReadLock后, 通過(guò)首先調(diào)用讀取模式EnterReadLock方法,然后再調(diào)用 ExitUpgradeableReadLock 方法。

具體的代碼示例請(qǐng)參考我的提問(wèn)及 dz45693 的文章

九、Mutex

Mutex 同Event,Semaphore類似,可以跨進(jìn)程同步內(nèi)容,定義跨進(jìn)程的Mutex只需要在初始化時(shí)為其指定名字即可;都繼承自WaitHandle,所以也有waitone的方法可以調(diào)用。

使用上與 Monitor 類似,屬于互斥鎖,所以在任務(wù)的最后必須要調(diào)用 ReleaseMutex()。

Mutex 實(shí)現(xiàn)了IDispose接口,所以需要在finally塊內(nèi)調(diào)用 Dispose() 方法。

Mutex可以用來(lái)限定winform程序只能有一個(gè)實(shí)例運(yùn)行,實(shí)例如下:

 static void Main()
 {
   bool runone;
   //獲取名為 single_test的互斥的初始所有權(quán),runone指定是否成功
   Mutex run = new Mutex(true, "single_test", out runone);
   if (runone) //true代表當(dāng)前未創(chuàng)建改互斥
   {
     run.ReleaseMutex();
     Application.EnableVisualStyles();
     Application.SetCompatibleTextRenderingDefault(false);
     FrmRemote frm = new FrmRemote();
     int hdc = frm.Handle.ToInt32(); // write to ...
     Application.Run(frm);
     IntPtr a = new IntPtr(hdc);
   }
   else
   {
     MessageBox.Show("已經(jīng)運(yùn)行了一個(gè)實(shí)例了。");              
   }
 }

十、ThreadLocal ,AsyncLocal,Volatile

在定義一個(gè)類時(shí),有時(shí)會(huì)定義全局變量,如果在編寫類時(shí)未考慮在多線程中使用,那么在類中定義的全局變量很有可能會(huì)因?yàn)槎嗑€程調(diào)用引發(fā)異常,比如文章開(kāi)頭的引子中,將 num 定義為了類的全局變量,造成多線程調(diào)用時(shí)出現(xiàn)錯(cuò)誤的情況,所以需要減少全局變量的使用。但是,有時(shí)候,我們又不得不借助全局變量幫助我們實(shí)現(xiàn)需求,這時(shí)候就可以考慮上面提到的這幾個(gè)。

我們可能希望定義的變量對(duì)每個(gè)線程是唯一的,這時(shí)候就可以借助ThreadLocal,如果是使用了async,await的寫法,因?yàn)樵赼wait之后執(zhí)行線程會(huì)發(fā)生變化,這時(shí)候就可以使用AsyncLocal,只是需要注意一下變量在父子進(jìn)程間的傳遞關(guān)系是怎么樣的。

AsyncLocal變量可以在父子線程中傳遞,創(chuàng)建子線程時(shí)父線程會(huì)將自己的AsyncLocal類型的上下文變量賦值到子線程中,但是,當(dāng)子線程改變線程上下文中AsnycLocal變量值后,父線程不會(huì)同步改變。也就是說(shuō)AsnycLocal變量只會(huì)影響他的子線程,不會(huì)影響他的父級(jí)線程。ThreadLocal只是當(dāng)前線程的上下文變量,不能在父子線程間同步。

​ 具體可看 AsnycLocal與ThreadLocal

至于Volatile,在 c # 中,對(duì) volatile 字段使用修飾符可保證對(duì)該字段的每個(gè)訪問(wèn)都是易失性內(nèi)存操作。我們知道vs在release模式下編譯時(shí)會(huì)對(duì)代碼進(jìn)行優(yōu)化,優(yōu)化的過(guò)程中可能會(huì)修改一些內(nèi)容,volatile修飾的字段則不會(huì)被編譯器進(jìn)行優(yōu)化。除此之外,多線程協(xié)作時(shí),希望對(duì)某一個(gè)變量的修改可以立即反饋到其他線程中,這時(shí)候也可以借助 volatile,但 volatile 修飾符不能應(yīng)用于數(shù)組元素。 Volatile.ReadVolatile.Write 方法可用于數(shù)組元素。

volatile 值立馬反饋到其他線程是因?yàn)樘幚肀粯?biāo)記字段時(shí),處理器不會(huì)使用緩存,而是每次都去內(nèi)存里讀取該字段。

至于處理器緩存之類的,如果有興趣,可以自行了解。

AsyncLocal 和 volatile 我自己并沒(méi)有實(shí)際用過(guò),只是在網(wǎng)上看了一些內(nèi)容,在官方文檔看了一點(diǎn)內(nèi)容,如有需要建議自行搜索。

十一、有意思的示例

記得之前看過(guò)一個(gè)例子,兩個(gè)線程循環(huán)輸出文字,不記得在哪看的了,試著寫一下

public class Sample
    {   //先執(zhí)行的線程設(shè)置為 true
        ManualResetEvent even = new ManualResetEvent(true);
        ManualResetEvent odd = new ManualResetEvent(false);        
        
        public void Sum()
        {           
            var ta = Task.Run(() => PrintEven(even, odd));
            var tb = Task.Run(() => PrintOdd (even,odd));                      
        }
				//等待自己的信號(hào),控制另一個(gè)線程的信號(hào)
        public void PrintEven(EventWaitHandle evenHandle, EventWaitHandle oddHandle)
        {            
            string design =  "偶數(shù)";
            for (int i = 0; i <= 20; i++)
            {               
                evenHandle.WaitOne();
                if ((i & 1) == 0)
                {
                    Console.WriteLine($"{design}:{i}");
                    evenHandle.Reset();
                    oddHandle.Set();
                }
            }
        }
        public void PrintOdd(EventWaitHandle evenHandle, EventWaitHandle oddHandle)
        {
            string design = "奇數(shù)";
            for (int i = 0; i <= 20; i++)
            {               
                oddHandle.WaitOne();
                if ((i & 1) == 1)
                {
                    Console.WriteLine($"{design}:{i}");
                    oddHandle.Reset();
                    evenHandle.Set();
                }
            }
        }
    }

​流水賬似的寫下了任務(wù)同步的實(shí)現(xiàn)方法,于我自己而言,對(duì)文章中提到的內(nèi)容加深了很多理解,希望對(duì)讀到文章的你也有幫助。

總結(jié)

到此這篇關(guān)于c#中多線程間的同步的文章就介紹到這了,更多相關(guān)c#多線程間同步內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • C#創(chuàng)建一個(gè)可快速重復(fù)使用的項(xiàng)目模板(詳細(xì)過(guò)程)

    C#創(chuàng)建一個(gè)可快速重復(fù)使用的項(xiàng)目模板(詳細(xì)過(guò)程)

    這篇文章主要介紹了C#如何創(chuàng)建一個(gè)可快速重復(fù)使用的項(xiàng)目模板今天給大家介紹的是基于官方的cli donet new 命令創(chuàng)建自己的項(xiàng)目模板,需要的朋友可以參考下
    2024-06-06
  • C#表達(dá)式和運(yùn)算符詳細(xì)解析

    C#表達(dá)式和運(yùn)算符詳細(xì)解析

    這篇文章主要介紹了C#表達(dá)式和運(yùn)算符詳細(xì)解析,文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下
    2022-07-07
  • C#實(shí)現(xiàn)將網(wǎng)址生成二維碼圖片方法介紹

    C#實(shí)現(xiàn)將網(wǎng)址生成二維碼圖片方法介紹

    這篇文章介紹了C#實(shí)現(xiàn)將網(wǎng)址生成二維碼圖片的方法,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2022-04-04
  • C# 多線程中經(jīng)常訪問(wèn)同一資源可能造成哪些問(wèn)題

    C# 多線程中經(jīng)常訪問(wèn)同一資源可能造成哪些問(wèn)題

    這篇文章主要介紹了C# 多線程中經(jīng)常訪問(wèn)同一資源可能造成哪些問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-04-04
  • C#導(dǎo)出文本內(nèi)容到word文檔的方法

    C#導(dǎo)出文本內(nèi)容到word文檔的方法

    這篇文章主要介紹了C#導(dǎo)出文本內(nèi)容到word文檔的方法,涉及C#操作word文檔的相關(guān)技巧,需要的朋友可以參考下
    2015-04-04
  • C#開(kāi)發(fā)答題贏錢游戲(自動(dòng)答題器)

    C#開(kāi)發(fā)答題贏錢游戲(自動(dòng)答題器)

    現(xiàn)在最火的直播游戲,那就是答題贏錢直播了,如百萬(wàn)英雄、芝士超人、花椒直播、沖頂大會(huì)等等,這些游戲的玩法都很簡(jiǎn)單,答對(duì)12題即可瓜分獎(jiǎng)金了。玩法雖簡(jiǎn)單但是完全答對(duì)12題難度就挺高了,下面小編給大家?guī)?lái)了C#開(kāi)發(fā)答題贏錢游戲,需要的朋友參考下吧
    2018-01-01
  • C#中的Explicit和Implicit詳情

    C#中的Explicit和Implicit詳情

    Implicit提高了代碼的可讀性,但程序員需要自己保證轉(zhuǎn)換不引發(fā)異常且不丟失信息、Explicit可阻止編譯器靜默調(diào)用可能產(chǎn)生意外后果的轉(zhuǎn)換操作。前者更易于使用,后者能向閱讀代碼的每個(gè)人清楚地指示您要轉(zhuǎn)換類型,下面就和小編來(lái)一起學(xué)習(xí)吧
    2021-09-09
  • C#實(shí)現(xiàn)圖形區(qū)域組合操作的方法

    C#實(shí)現(xiàn)圖形區(qū)域組合操作的方法

    這篇文章主要介紹了C#實(shí)現(xiàn)圖形區(qū)域組合操作的方法,涉及C#操作圖片實(shí)現(xiàn)組合操作的相關(guān)技巧,需要的朋友可以參考下
    2015-06-06
  • C#實(shí)現(xiàn)FTP客戶端的案例

    C#實(shí)現(xiàn)FTP客戶端的案例

    這篇文章主要為大家詳細(xì)介紹了C#實(shí)現(xiàn)FTP客戶端的小案例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-07-07
  • 詳解C#對(duì)Dictionary內(nèi)容的通用操作

    詳解C#對(duì)Dictionary內(nèi)容的通用操作

    這篇文章主要為大家詳細(xì)介紹了C#對(duì)Dictionary內(nèi)容的一些通用操作,例如:根據(jù)鍵移除信息、根據(jù)值移除信息、根據(jù)鍵獲取值等,需要的可以參考一下
    2022-06-06

最新評(píng)論