您好,登錄后才能下訂單哦!
本篇內容介紹了“如何理解c#中多線程間”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
一、引入
二、Lock
三、Monitor
四、Interlocked
五、Semaphore
六、Event
七、Barrier
八、ReaderWriterLockSlim
九、Mutex
十、ThreadLocal ,AsyncLocal,Volatile
十一、有意思的示例
總結
先給出一個Num類的定義
internal class Num { public static int odd = 50000; public static int even = 10000; }
假設現在要求輸出小于 odd 的所有奇數,輸出小于 even 的所有偶數,不考慮多線程時可以寫出如下的代碼:(為了演示多線程時線程間的爭用,先把值賦給了 num,實際上這個賦值操作毫無意義 )
//同步代碼段 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($"奇數:{num}"); } } for (int i = 0; i <= Num.even; i++) { num = i; if ((i & 1) == 0) { Thread.Sleep(10); Console.WriteLine($"偶數:{num}"); } } sw.Stop(); return sw.ElapsedMilliseconds; }
現在,因為耗時太長,引入多線程進行處理,修改為如下形式:
//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; //判斷條件之前賦值是為了提高觸發幾率 if((i & 1) == 1) { Console.WriteLine($"奇數:{num}"); } } }); var tb = Task.Run(() => { for (int i = 0; i <= Num.even; i++) { num = i; if ((i & 1) == 0) { Thread.Sleep(10); //在此處添加延時,在 tb 線程等待時,num.sum的值可能已經被 ta 修改為了其他值 Console.WriteLine($"偶數:{num}"); } } }); Task.WaitAll(ta, tb); //為了保證任務完成,獲取執行時間 sw.Stop(); return sw.ElapsedMilliseconds; } }
上面這段代碼中,我們期望線程ta會輸出odd以內的奇數值,線程tb會輸出even以內的偶數。但實際運行時會出現下圖所示的情況。
如上,當程序涉及到多線程的時候,在 [各線程間共享的數據] 總會因為線程間的爭用導致意料之外的情況,為此,各種語言也會提供協助線程間同步的特性,這里簡單記錄一下我對c#中機制的理解。
借用Lock的方法對示例進行修改,可以有兩種方式,此處展示ta部分,tb 與ta做相同修改:
//方式一:在Task內全局Lock var ta = Task.Run(() => { lock (sync) { for (int i = 0; i <= 10000; i++) { num = i; //判斷條件之前賦值是為了提高觸發幾率 if ((i & 1) == 1) { Console.WriteLine($"奇數:{num}"); } } } }); //方式二: 為每一次For循環Lock var ta = Task.Run(() => { //lock (sync) for (int i = 0; i <= Num.odd; i++) { lock(sync) { num = i; //判斷條件之前賦值是為了提高觸發幾率 if ((i & 1) == 1) { Console.WriteLine($"奇數:{num}"); } } } });
上述的兩種方式中,
方法一相當于對Task進行了鎖定,同一時刻只能運行一個被鎖定的代碼段(即取決于當前對象實例中ta,tb誰先取得了使用權),這樣多線程其實退化為了單線程處理。
方法二應該是更合理的使用方式,每一次循環時進行鎖定,保證了每次賦值及使用時的獨占性,也不影響另一個線程的循環操作。方式二仍然會存在一個線程等待的情況,只是會比第一種方式好一些。但是,對每一次循環進行Lock,性能是需要考慮的一個點。
說回Lock本身,網上有很多文章介紹,lock只是一個語法糖,編譯器會將其轉換為對 monitor 的調用。
IL代碼如下圖:
可以看到,編譯器會幫我們構建try塊,并在finally塊調用Monitor.Exit方法。若要獲取更精細的控制,可以自己調用Monitor進行使用。
monitor與lock相比更為靈活,可以使用IsEntered(object)判斷當前線程是否獲取到了sync的鎖定,可以使用 TryEnter()嘗試獲取排它鎖,也可以調用重載方法指定等待時間。
需要指出的是,
1、所有等待獲取鎖定的線程會處于阻塞狀態。
2、在等待獲取鎖定的線程上執行Thread.Interrupt會中斷當前線程的等待并拋出ThreadInterruptedException的異常
3、monitor與lock鎖定的對象sync必須為引用類型,查看反編譯的代碼會發現在每一次lock之前,會將sync賦值給一個object對象,如果sync為值類型,則會被裝箱為一個新的對象。
4、以鎖定For循環為例,在定義鎖定對象時,可以定義為
public class LockFor { private readonly object sync = new(); .... }
或者定義為
public class LockForStaticSync { private static readonly object sync = new(); ... }
后者添加一個 static 修飾符將其定義靜態只讀對象。
我們知道,static 修飾的變量并不屬于對象,而是歸屬于 class 本身,按照我的理解來說,如果在多個線程中都實例化了 LockForStaticSync 的對象,并同時調用一個 SUM 方法時,不論線程間是否會發生沖突,只能有一個線程取得sync的鎖定繼續執行,其他的線程會處于阻塞等待狀態。
而如果是第一種定義方式,則多個線程的多個對象間,不會產生沖突,所有線程都可以執行。
此例的場景下,多實例調用時,方式一應該會有速度上的絕對優勢。為此我刪除了任務內打印及等待的部分,執行了如下內容進行驗證:
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); }
但實際執行結果是出乎意料的,多次運行后,類變量鎖的運行速度遠超對象變量的方式,這是為什么呢?
思考之后,考慮到類變量省略了每次創建鎖定對象的時間,而數量較少的循環次數可能無法彌補這個時間差,于是我逐漸調大Num類中定義的變量。隨著循環次數的增加,也就是方法執行時間的增加,對象變量的優勢逐漸顯現
既然對象變量有速度上的優勢,而使用過程中又不可避免的會出現多方調用的情況,那么是不是應該一直選擇定義為對象變量呢?其實不然,比如靜態類中需要的鎖定對象,全局緩存字典,文件操作幫助類都應該是靜態鎖,更多場景,歡迎補充。
Interlocked類中的方法可以實現原子操作,通過操作系統及硬件CPU級別的控制,確保CPU在執行當前操作時不會被中斷,這個類里面提供了一些簡單的方法,如Add,Increment等。
信號量是一種計數的互斥鎖定。什么意思呢?以Monitor來講,從monitor.enetr開始,到monitor.exit為止,被包裹著的這一段代碼,同一時刻只能由一個線程訪問,而 Semaphore 可以定義同時訪問某些資源的線程數量,即允許多線程同時訪問被保護的代碼。
Semaphore有三種簽名的構造函數,其中 Semaphore(int initialCount, int maximumCount) 的參數指定最初釋放的信號量可用數量與最大量,兩者的差值歸創建信號量的線程所有。
以下內容來自 MSDN
class SemaphoreTest { // A semaphore that simulates a limited resource pool. private static Semaphore _pool; // 協助設置線程休眠時間. private static int _padding; public static void Main() { // 這里是創建了最大可以有三個訪問線程的信號量,但此時可用為0,需要當前線程釋放以后才可用。 // 如果此處設置可用量非0,主線程釋放時仍然傳遞了3,程序運行過程中會出現SemaphoreFullException的異常 _pool = new Semaphore(0, 3); for (int i = 1; i <= 5; i++) { Thread t = new Thread(new ParameterizedThreadStart(Worker)); t.Start(i); } // 主線程休眠,讓其他線程運行到等待信號量的狀態 Thread.Sleep(500); //主線程調用Release(3),將可用量設置為最大值,正在等待的線程會獲得信號 Console.WriteLine("Main thread calls Release(3)."); _pool.Release(3); Console.WriteLine("Main thread exits."); Console.ReadLine(); } private static void Worker(object num) { // 阻塞當前線程,直到獲取到信號量 Console.WriteLine("Thread {0} begins " +"and waits for the semaphore.", num); _pool.WaitOne(); // 每一個線程等待的時間間隔參數 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()); } }
目前為止,提到的內容均是在同一個進程內同步的方法,信號量作為一個系統級的存在,是可以幫助我們實現進程間同步的,只需要在創建 Seamphore 對象的實例時,為信號量指定名稱即可。
另外特別需要注意的是,信號量是可重入的,簡單說就是可以在一個線程內執行多次 WaitOne() 方法,多次調用時,如果處理不好,就可能出現意外情況,修改 Worker 為下方內容
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(); //此處添加一處調用 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()); //這里應該再調用一次_pool.Release(),或者在上一句傳入參數,改為 _pool.Release(2) }
建議實際運行一下,查看運行的表現,運行過后會發現,任務線程全部阻塞在了第二處 WaitOne,導致程序無法向下執行。
因此,重入必須要謹慎,而且退出時必須要釋放等量的鎖定數值,如果執行了多余的Release(),最終程序在運行過程中會出現
SemaphoreFullException的異常
與信號量一樣,事件也是一個系統范圍內的資源同步方法。又分為ManualResetEvent,AutoResetEvent,CountdownEvent以及ManualResetEventSlim,在構建對象實例時若傳入了name參數,代表這是一個可以跨進程的系統級同步事件。
以 ManualResetEvent 為例,該類有 signaled 和 nonsignaled 兩種狀態,這兩種狀態通過實例化對象時的 布爾類型 參數決定,TRUE 就是signaled, False相反
文檔中常見的翻譯是發出信號的狀態和未發出信號的狀態,微軟官網的機翻是終止狀態和非終止狀態,還有一些釋放線程之類的描述,直觀上難以理解。其實就是改變狀態而已,還不如英文的好理解。
ManualResetEvent 的基類 EventWaitHandle 中提供了Set() 和Reset() 方法,用于改變狀態,Set 將事件修改為 signaled,Reset重置為 nonsignaled
這里說一下Set和Reset,這兩個方法是改變了事件的狀態,并不是一個瞬時性的動作,也就意味著在調用Set后,調用Reset之前,事件都處于 signaled 狀態(AutoResetEvent會自動調用Reset重置事件狀態)
WaitHandle 類中提供了眾多等待信號的方法,EventWaitHandle 繼承自WaitHandle, ManualResetEvent中也可以調用WaitOne等方法。
回頭再看一下信號量 Semaphore 的示例,也調用 WaitOne 等待信號,因為它也繼承自 Waithandler。
有了上面的基礎,可以看一下下面 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都會處于等待狀態,直至 mre修改狀態,按 Enter繼續"); Console.ReadLine(); mre.Set(); Thread.Sleep(500); Console.WriteLine(@"調用了mre.set后,事件處于 signaled 狀態,下一個線程不會被阻塞"); 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("調用mre.reset后,事件處于nonsignaled狀態,此時會被阻塞"); 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,名字可以看出來這個類會自動調用 Reset方法,事實也是這樣,這個類會在等待線程執行結束后將事件置為non-signaled
ManualResetEventSlim 是 ManualResetEvent的輕量實現,他并不是繼承自 EventWaitHandle基類,
CountdownEvent 也不是繼承自 EventWaitHandle基類,它會在初始化時得到一個數值,稱為InitialCount,同時賦給CurrentCount , 每次調用Signal時,CurrentCount會減少相應的值,當調用后CurrentCount為0時,會發出信號,并將其設置為 IS_SET 狀態。
Barrier 是一個有意思的類,可以使多個任務能夠采用并行方式依據某種算法在多個階段中協同工作。
Enables multiple tasks to cooperatively work on an algorithm in parallel through multiple phases.
大白話來講,借助于這個對象,可以管控多個并行任務間的合作關系。
Barrier的構造函數中可以傳入并行任務的數量(participantCount),也可以通過 AddParticipant,RemoveParticipant 動態地調整參與者的數量。另外可以通過 CurrentPhaseNumber 得知當前是第幾個參與者,通過 ParticipantsRemaining 知道還有幾個參與者未到達任務點。
還可以傳入一個可空委托,這個委托會在接收到所有參與者線程發出信號后執行。
就好像幾個人一起玩游戲闖關,關卡boss需要所有人一起才能打敗,照顧到每個人的游戲理解不同,規定每個人可以按照自己的安排推進游戲進度。
在這個比喻中,每個人都是單獨的線程,可能有人很快就抵達了關卡,但是因為關卡的性質,他必須在這里等待其他人都到達后,大家一起打boss,打敗boss之后存檔,之后大家再各玩各的,直到下一個關卡BOSS。
Barrier就承擔了boss的任務,他負責讓所有線程抵達某一個預設的點后再一起放行。放行之后,會執行初始化對象時傳入的委托,比如上方說的存檔。只是我們可以通過委托,更靈活地指定要進行的操作。
而所謂的預設點,其實就是調用 SignalAndWait,發出信號并等待其他線程發出信號的代碼
如果你暫時沒有理解我上面的比喻,那就看一下下面的代碼吧,代碼是在MSDN拿來的,我自己加了幾個console語句,可以運行看看效果
public static void Barriers() { int count = 0; //初始化 3 個參與者,傳入委托 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個 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會引發委托里拋出的異常,異常信息所有線程可見 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}"); }; // 啟動與 Barrier設置數量相同的任務,如果啟動數目超過設置值,會引發如下異常 //"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(); }
執行效果如下:
可以在24行,27行打斷點,借助上面的比喻,理解一下Barrier的用法。
最后要聲明的是Barrier的public protected 成員是線程安全的,可以跨線程使用。但是Dispose是非線程安全的,意味著一旦調用,所有線程都會受到影響,應該在任務代碼之外執行,另外既然有dispose的方法,就要注意使用完畢后調用該方法釋放資源。
讀寫鎖,允許多個線程處于讀取模式,允許一個線程處于具有獨占鎖定權限的寫入模式,并且允許具有讀取訪問權限的一個線程處于可升級讀取模式,在該模式下,線程可以升級到寫入模式,而無需放棄對資源的讀取訪問權限。
讀寫鎖具有三種模式,讀鎖,寫鎖,可升級的讀鎖
讀鎖可以通過 EnterReadLock 進入,通過 ExitReadLock 退出,寫鎖類似EnterWriteLock,ExitWriteLock
可升級的讀鎖是指可以直接由讀轉換為寫模式的狀態,EnterUpgradeableReadLock及ExitUpgradeableReadLock
與其他同步對象相同,讀寫鎖需要正確的進行釋放,不然會引發問題
讀寫鎖初始化時,可以傳入 LockRecursionPolicy 指定遞歸狀態,默認的構造函數為NoRecursion,微軟官網并不建議新手使用遞歸策略,因為這具有更高的復雜性,而且容易帶來死鎖的問題,我自己也沒有用過遞歸策略。
與ReaderWriterLock相比,ReaderWriterLockSlim是被推薦使用的對象
對于 ReaderWriterLockSlim 的使用,我在園子里有個提問請教ReaderWriterLockSlim的問題,建議轉過去看下,我這邊就不放代碼了,當時找到了另一篇文章解答了我的疑惑,地址也貼在這里 C# ReaderWriterLockSlim 實現 - dz45693.
這里面就有一些需要注意的點,這幾個點全部是摘抄自上面那篇文章,請知悉:
對于同一把鎖、多個線程可同時進入讀模式。
對于同一把鎖、同時只允許一個線程進入寫模式。
對于同一把鎖、同時只允許一個線程進入可升級的讀模式。
通過默認構造函數創建的讀寫鎖是不支持遞歸的,若想支持遞歸 可通過構造 ReaderWriterLockSlim(LockRecursionPolicy) 創建實例。
對于同一把鎖、同一線程不可兩次進入同一鎖狀態(開啟遞歸后可以)
對于同一把鎖、即便開啟了遞歸、也不可以在進入讀模式后再次進入寫模式或者可升級的讀模式(在這之前必須退出讀模式)。
再次強調、不建議啟用遞歸。
讀寫鎖具有線程關聯性,即兩個線程間擁有的鎖的狀態相互獨立不受影響、并且不能相互修改其鎖的狀態。
升級狀態:在進入可升級的讀模式 EnterUpgradeableReadLock后,可在恰當時間點通過EnterWriteLock進入寫模式。
降級狀態:可升級的讀模式可以降級為讀模式:即在進入可升級的讀模式EnterUpgradeableReadLock后, 通過首先調用讀取模式EnterReadLock方法,然后再調用 ExitUpgradeableReadLock 方法。
具體的代碼示例請參考我的提問及 dz45693 的文章
Mutex 同Event,Semaphore類似,可以跨進程同步內容,定義跨進程的Mutex只需要在初始化時為其指定名字即可;都繼承自WaitHandle,所以也有waitone的方法可以調用。
使用上與 Monitor 類似,屬于互斥鎖,所以在任務的最后必須要調用 ReleaseMutex()。
Mutex 實現了IDispose接口,所以需要在finally塊內調用 Dispose() 方法。
Mutex可以用來限定winform程序只能有一個實例運行,實例如下:
static void Main() { bool runone; //獲取名為 single_test的互斥的初始所有權,runone指定是否成功 Mutex run = new Mutex(true, "single_test", out runone); if (runone) //true代表當前未創建改互斥 { 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("已經運行了一個實例了。"); } }
在定義一個類時,有時會定義全局變量,如果在編寫類時未考慮在多線程中使用,那么在類中定義的全局變量很有可能會因為多線程調用引發異常,比如文章開頭的引子中,將 num 定義為了類的全局變量,造成多線程調用時出現錯誤的情況,所以需要減少全局變量的使用。但是,有時候,我們又不得不借助全局變量幫助我們實現需求,這時候就可以考慮上面提到的這幾個。
我們可能希望定義的變量對每個線程是唯一的,這時候就可以借助ThreadLocal,如果是使用了async,await的寫法,因為在await之后執行線程會發生變化,這時候就可以使用AsyncLocal,只是需要注意一下變量在父子進程間的傳遞關系是怎么樣的。
AsyncLocal變量可以在父子線程中傳遞,創建子線程時父線程會將自己的AsyncLocal類型的上下文變量賦值到子線程中,但是,當子線程改變線程上下文中AsnycLocal變量值后,父線程不會同步改變。也就是說AsnycLocal變量只會影響他的子線程,不會影響他的父級線程。ThreadLocal只是當前線程的上下文變量,不能在父子線程間同步。
具體可看 AsnycLocal與ThreadLocal
至于Volatile,在 c # 中,對 volatile 字段使用修飾符可保證對該字段的每個訪問都是易失性內存操作。我們知道vs在release模式下編譯時會對代碼進行優化,優化的過程中可能會修改一些內容,volatile修飾的字段則不會被編譯器進行優化。除此之外,多線程協作時,希望對某一個變量的修改可以立即反饋到其他線程中,這時候也可以借助 volatile,但 volatile 修飾符不能應用于數組元素。 Volatile.Read和 Volatile.Write 方法可用于數組元素。
volatile 值立馬反饋到其他線程是因為處理被標記字段時,處理器不會使用緩存,而是每次都去內存里讀取該字段。
至于處理器緩存之類的,如果有興趣,可以自行了解。
AsyncLocal 和 volatile 我自己并沒有實際用過,只是在網上看了一些內容,在官方文檔看了一點內容,如有需要建議自行搜索。
記得之前看過一個例子,兩個線程循環輸出文字,不記得在哪看的了,試著寫一下
public class Sample { //先執行的線程設置為 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)); } //等待自己的信號,控制另一個線程的信號 public void PrintEven(EventWaitHandle evenHandle, EventWaitHandle oddHandle) { string design = "偶數"; 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 = "奇數"; for (int i = 0; i <= 20; i++) { oddHandle.WaitOne(); if ((i & 1) == 1) { Console.WriteLine($"{design}:{i}"); oddHandle.Reset(); evenHandle.Set(); } } } }
“如何理解c#中多線程間”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。