目錄git
線程基礎主要包括線程建立、掛起、等待和終止線程。關於更多的線程的底層實現,CPU時間片輪轉等等的知識,能夠參考《深刻理解計算機系統》
一書中關於進程和線程的章節,本文不過多贅述。算法
在C#語言中,建立線程是一件很是簡單的事情;它只須要用到 System.Threading
命名空間,其中主要使用Thread
類來建立線程。編程
演示代碼以下所示:c#
using System; using System.Threading; // 建立線程須要用到的命名空間 namespace Recipe1 { class Program { static void Main(string[] args) { // 1.建立一個線程 PrintNumbers爲該線程所須要執行的方法 Thread t = new Thread(PrintNumbers); // 2.啓動線程 t.Start(); // 主線程也運行PrintNumbers方法,方便對照 PrintNumbers(); // 暫停一下 Console.ReadKey(); } static void PrintNumbers() { // 使用Thread.CurrentThread.ManagedThreadId 能夠獲取當前運行線程的惟一標識,經過它來區別線程 Console.WriteLine($"線程:{Thread.CurrentThread.ManagedThreadId} 開始打印..."); for (int i = 0; i < 10; i++) { Console.WriteLine($"線程:{Thread.CurrentThread.ManagedThreadId} 打印:{i}"); } } } }
運行結果以下圖所示,咱們能夠經過運行結果得知上面的代碼建立了一個線程,而後主線程和建立的線程交叉輸出結果,這說明PrintNumbers
方法同時運行在主線程和另一個線程中。安全
暫停線程這裏使用的方式是經過Thread.Sleep
方法,若是線程執行Thread.Sleep
方法,那麼操做系統將在指定的時間內不爲該線程分配任什麼時候間片。若是Sleep時間100ms那麼操做系統將至少讓該線程睡眠100ms或者更長時間,因此Thread.Sleep
方法不能做爲高精度的計時器使用。數據結構
演示代碼以下所示:多線程
using System; using System.Threading; // 建立線程須要用到的命名空間 namespace Recipe2 { class Program { static void Main(string[] args) { // 1.建立一個線程 PrintNumbers爲該線程所須要執行的方法 Thread t = new Thread(PrintNumbersWithDelay); // 2.啓動線程 t.Start(); // 暫停一下 Console.ReadKey(); } static void PrintNumbersWithDelay() { Console.WriteLine($"線程:{Thread.CurrentThread.ManagedThreadId} 開始打印... 如今時間{DateTime.Now.ToString("HH:mm:ss.ffff")}"); for (int i = 0; i < 10; i++) { //3. 使用Thread.Sleep方法來使當前線程睡眠,TimeSpan.FromSeconds(2)表示時間爲 2秒 Thread.Sleep(TimeSpan.FromSeconds(2)); Console.WriteLine($"線程:{Thread.CurrentThread.ManagedThreadId} 打印:{i} 如今時間{DateTime.Now.ToString("HH:mm:ss.ffff")}"); } } } }
運行結果以下圖所示,經過下圖能夠肯定上面的代碼是有效的,經過Thread.Sleep
方法,使線程休眠了2秒左右,可是並非特別精確的2秒。驗證了上面的說法,它的睡眠是至少讓線程睡眠多長時間,而不是必定多長時間。閉包
在本章中,線程等待使用的是Join
方法,該方法將暫停執行當前線程,直到所等待的另外一個線程終止。在簡單的線程同步中會使用到,但它比較簡單,不做過多介紹。ide
演示代碼以下所示:函數
class Program { static void Main(string[] args) { Console.WriteLine($"-------開始執行 如今時間{DateTime.Now.ToString("HH:mm:ss.ffff")}-------"); // 1.建立一個線程 PrintNumbersWithDelay爲該線程所須要執行的方法 Thread t = new Thread(PrintNumbersWithDelay); // 2.啓動線程 t.Start(); // 3.等待線程結束 t.Join(); Console.WriteLine($"-------執行完畢 如今時間{DateTime.Now.ToString("HH:mm:ss.ffff")}-------"); // 暫停一下 Console.ReadKey(); } static void PrintNumbersWithDelay() { Console.WriteLine($"線程:{Thread.CurrentThread.ManagedThreadId} 開始打印... 如今時間{DateTime.Now.ToString("HH:mm:ss.ffff")}"); for (int i = 0; i < 10; i++) { Thread.Sleep(TimeSpan.FromSeconds(2)); Console.WriteLine($"線程:{Thread.CurrentThread.ManagedThreadId} 打印:{i} 如今時間{DateTime.Now.ToString("HH:mm:ss.ffff")}"); } } }
運行結果以下圖所示,開始執行和執行完畢兩條信息由主線程打印;根據其輸出的順序可見主線程是等待另外的線程結束後才輸出執行完畢這條信息。
終止線程使用的方法是Abort
方法,當該方法被執行時,將嘗試銷燬該線程。經過引起ThreadAbortException
異常使線程被銷燬。但通常不推薦使用該方法,緣由有如下幾點。
- 使用
Abort
方法只是嘗試銷燬該線程,但不必定能終止線程。- 若是被終止的線程在執行lock內的代碼,那麼終止線程會形成線程不安全。
- 線程終止時,CLR會保證本身內部的數據結構不會損壞,可是BCL不能保證。
基於以上緣由不推薦使用Abort
方法,在實際項目中通常使用CancellationToken
來終止線程。
演示代碼以下所示:
static void Main(string[] args) { Console.WriteLine($"-------開始執行 如今時間{DateTime.Now.ToString("HH:mm:ss.ffff")}-------"); // 1.建立一個線程 PrintNumbersWithDelay爲該線程所須要執行的方法 Thread t = new Thread(PrintNumbersWithDelay); // 2.啓動線程 t.Start(); // 3.主線程休眠6秒 Thread.Sleep(TimeSpan.FromSeconds(6)); // 4.終止線程 t.Abort(); Console.WriteLine($"-------執行完畢 如今時間{DateTime.Now.ToString("HH:mm:ss.ffff")}-------"); // 暫停一下 Console.ReadKey(); } static void PrintNumbersWithDelay() { Console.WriteLine($"線程:{Thread.CurrentThread.ManagedThreadId} 開始打印... 如今時間{DateTime.Now.ToString("HH:mm:ss.ffff")}"); for (int i = 0; i < 10; i++) { Thread.Sleep(TimeSpan.FromSeconds(2)); Console.WriteLine($"線程:{Thread.CurrentThread.ManagedThreadId} 打印:{i} 如今時間{DateTime.Now.ToString("HH:mm:ss.ffff")}"); } }
運行結果以下圖所示,啓動所建立的線程3後,6秒鐘主線程調用了Abort
方法,線程3沒有繼續執行便結束了;與預期的結果一致。
線程的狀態可經過訪問ThreadState
屬性來檢測,ThreadState
是一個枚舉類型,一共有10種狀態,狀態具體含義以下表所示。
成員名稱 | 說明 |
---|---|
Aborted | 線程處於 Stopped 狀態中。 |
AbortRequested | 已對線程調用了 Thread.Abort 方法,但線程還沒有收到試圖終止它的掛起的 System.Threading.ThreadAbortException。 |
Background | 線程正做爲後臺線程執行(相對於前臺線程而言)。此狀態能夠經過設置 Thread.IsBackground 屬性來控制。 |
Running | 線程已啓動,它未被阻塞,而且沒有掛起的 ThreadAbortException。 |
Stopped | 線程已中止。 |
StopRequested | 正在請求線程中止。這僅用於內部。 |
Suspended | 線程已掛起。 |
SuspendRequested | 正在請求線程掛起。 |
Unstarted | 還沒有對線程調用 Thread.Start 方法。 |
WaitSleepJoin | 因爲調用 Wait、Sleep 或 Join,線程已被阻止。 |
下表列出致使狀態更改的操做。
操做 | ThreadState |
---|---|
在公共語言運行庫中建立線程。 | Unstarted |
線程調用 Start | Unstarted |
線程開始運行。 | Running |
線程調用 Sleep | WaitSleepJoin |
線程對其餘對象調用 Wait。 | WaitSleepJoin |
線程對其餘線程調用 Join。 | WaitSleepJoin |
另外一個線程調用 Interrupt | Running |
另外一個線程調用 Suspend | SuspendRequested |
線程響應 Suspend 請求。 | Suspended |
另外一個線程調用 Resume | Running |
另外一個線程調用 Abort | AbortRequested |
線程響應 Abort 請求。 | Stopped |
線程被終止。 | Stopped |
演示代碼以下所示:
static void Main(string[] args) { Console.WriteLine("開始執行..."); Thread t = new Thread(PrintNumbersWithStatus); Thread t2 = new Thread(DoNothing); // 使用ThreadState查看線程狀態 此時線程未啓動,應爲Unstarted Console.WriteLine($"Check 1 :{t.ThreadState}"); t2.Start(); t.Start(); // 線程啓動, 狀態應爲 Running Console.WriteLine($"Check 2 :{t.ThreadState}"); // 因爲PrintNumberWithStatus方法開始執行,狀態爲Running // 可是經接着會執行Thread.Sleep方法 狀態會轉爲 WaitSleepJoin for (int i = 1; i < 30; i++) { Console.WriteLine($"Check 3 : {t.ThreadState}"); } // 延時一段時間,方便查看狀態 Thread.Sleep(TimeSpan.FromSeconds(6)); // 終止線程 t.Abort(); Console.WriteLine("t線程被終止"); // 因爲該線程是被Abort方法終止 因此狀態爲 Aborted或AbortRequested Console.WriteLine($"Check 4 : {t.ThreadState}"); // 該線程正常執行結束 因此狀態爲Stopped Console.WriteLine($"Check 5 : {t2.ThreadState}"); Console.ReadKey(); } static void DoNothing() { Thread.Sleep(TimeSpan.FromSeconds(2)); } static void PrintNumbersWithStatus() { Console.WriteLine("t線程開始執行..."); // 在線程內部,可經過Thread.CurrentThread拿到當前線程Thread對象 Console.WriteLine($"Check 6 : {Thread.CurrentThread.ThreadState}"); for (int i = 1; i < 10; i++) { Thread.Sleep(TimeSpan.FromSeconds(2)); Console.WriteLine($"t線程輸出 :{i}"); } }
運行結果以下圖所示,與預期的結果一致。
Windows操做系統爲搶佔式多線程(Preemptive multithreaded)操做系統,是由於線程可在任什麼時候間中止(被槍佔)並調度另外一個線程。
Windows操做系統中線程有0(最低) ~ 31(最高)
的優先級,而優先級越高所能佔用的CPU時間就越多,肯定某個線程所處的優先級須要考慮進程優先級和相對線程優先級兩個優先級。
- 進程優先級:Windows支持6個進程優先級,分別是
Idle、Below Normal、Normal、Above normal、High 和Realtime
。默認爲Normal
。- 相對線程優先級:相對線程優先級是相對於進程優先級的,由於進程包含了線程。Windows支持7個相對線程優先級,分別是
Idle、Lowest、Below Normal、Normal、Above Normal、Highest 和 Time-Critical
.默認爲Normal
。
下表總結了進程的優先級和線程的相對優先級與優先級(0~31)的映射關係。粗體爲相對線程優先級,斜體爲進程優先級。
Idle | Below Normal | Normal | Above Normal | High | Realtime | |
---|---|---|---|---|---|---|
Time-Critical | 15 | 15 | 15 | 15 | 15 | 31 |
Highest | 6 | 8 | 10 | 12 | 15 | 26 |
Above Normal | 5 | 7 | 9 | 11 | 14 | 25 |
Normal | 4 | 6 | 8 | 10 | 13 | 24 |
Below Normal | 3 | 5 | 7 | 9 | 12 | 23 |
Lowest | 2 | 4 | 6 | 8 | 11 | 22 |
Idle | 1 | 1 | 1 | 1 | 1 | 16 |
而在C#程序中,可更改線程的相對優先級,須要設置Thread
的Priority
屬性,可設置爲ThreadPriority
枚舉類型的五個值之一:Lowest、BelowNormal、Normal、AboveNormal 或 Highest
。CLR爲本身保留了Idle
和Time-Critical
優先級,程序中不可設置。
演示代碼以下所示。
static void Main(string[] args) { Console.WriteLine($"當前線程優先級: {Thread.CurrentThread.Priority} \r\n"); // 第一次測試,在全部核心上運行 Console.WriteLine("運行在全部空閒的核心上"); RunThreads(); Thread.Sleep(TimeSpan.FromSeconds(2)); // 第二次測試,在單個核心上運行 Console.WriteLine("\r\n運行在單個核心上"); // 設置在單個核心上運行 System.Diagnostics.Process.GetCurrentProcess().ProcessorAffinity = new IntPtr(1); RunThreads(); Console.ReadLine(); } static void RunThreads() { var sample = new ThreadSample(); var threadOne = new Thread(sample.CountNumbers); threadOne.Name = "線程一"; var threadTwo = new Thread(sample.CountNumbers); threadTwo.Name = "線程二"; // 設置優先級和啓動線程 threadOne.Priority = ThreadPriority.Highest; threadTwo.Priority = ThreadPriority.Lowest; threadOne.Start(); threadTwo.Start(); // 延時2秒 查看結果 Thread.Sleep(TimeSpan.FromSeconds(2)); sample.Stop(); } class ThreadSample { private bool _isStopped = false; public void Stop() { _isStopped = true; } public void CountNumbers() { long counter = 0; while (!_isStopped) { counter++; } Console.WriteLine($"{Thread.CurrentThread.Name} 優先級爲 {Thread.CurrentThread.Priority,11} 計數爲 = {counter,13:N0}"); } }
運行結果以下圖所示。Highest
佔用的CPU時間明顯多於Lowest
。當程序運行在全部核心上時,線程能夠在不一樣核心同時運行,因此Highest
和Lowest
差距會小一些。
在CLR中,線程要麼是前臺線程,要麼就是後臺線程。當一個進程的全部前臺線程中止運行時,CLR將強制終止仍在運行的任何後臺線程,不會拋出異常。
在C#中可經過Thread
類中的IsBackground
屬性來指定是否爲後臺線程。在線程生命週期中,任什麼時候候均可從前臺線程變爲後臺線程。線程池中的線程默認爲後臺線程。
演示代碼以下所示。
static void Main(string[] args) { var sampleForeground = new ThreadSample(10); var sampleBackground = new ThreadSample(20); var threadPoolBackground = new ThreadSample(20); // 默認建立爲前臺線程 var threadOne = new Thread(sampleForeground.CountNumbers); threadOne.Name = "前臺線程"; var threadTwo = new Thread(sampleBackground.CountNumbers); threadTwo.Name = "後臺線程"; // 設置IsBackground屬性爲 true 表示後臺線程 threadTwo.IsBackground = true; // 線程池內的線程默認爲 後臺線程 ThreadPool.QueueUserWorkItem((obj) => { Thread.CurrentThread.Name = "線程池線程"; threadPoolBackground.CountNumbers(); }); // 啓動線程 threadOne.Start(); threadTwo.Start(); } class ThreadSample { private readonly int _iterations; public ThreadSample(int iterations) { _iterations = iterations; } public void CountNumbers() { for (int i = 0; i < _iterations; i++) { Thread.Sleep(TimeSpan.FromSeconds(0.5)); Console.WriteLine($"{Thread.CurrentThread.Name} prints {i}"); } } }
運行結果以下圖所示。當前臺線程10次循環結束之後,建立的後臺線程和線程池線程都會被CLR強制結束。
向線程中傳遞參數經常使用的有三種方法,構造函數傳值、Start方法傳值和Lambda表達式傳值,通常經常使用Start方法來傳值。
演示代碼以下所示,經過三種方式來傳遞參數,告訴線程中的循環最終須要循環幾回。
static void Main(string[] args) { // 第一種方法 經過構造函數傳值 var sample = new ThreadSample(10); var threadOne = new Thread(sample.CountNumbers); threadOne.Name = "ThreadOne"; threadOne.Start(); threadOne.Join(); Console.WriteLine("--------------------------"); // 第二種方法 使用Start方法傳值 // Count方法 接收一個Object類型參數 var threadTwo = new Thread(Count); threadTwo.Name = "ThreadTwo"; // Start方法中傳入的值 會傳遞到 Count方法 Object參數上 threadTwo.Start(8); threadTwo.Join(); Console.WriteLine("--------------------------"); // 第三種方法 Lambda表達式傳值 // 其實是構建了一個匿名函數 經過函數閉包來傳值 var threadThree = new Thread(() => CountNumbers(12)); threadThree.Name = "ThreadThree"; threadThree.Start(); threadThree.Join(); Console.WriteLine("--------------------------"); // Lambda表達式傳值 會共享變量值 int i = 10; var threadFour = new Thread(() => PrintNumber(i)); i = 20; var threadFive = new Thread(() => PrintNumber(i)); threadFour.Start(); threadFive.Start(); } static void Count(object iterations) { CountNumbers((int)iterations); } static void CountNumbers(int iterations) { for (int i = 1; i <= iterations; i++) { Thread.Sleep(TimeSpan.FromSeconds(0.5)); Console.WriteLine($"{Thread.CurrentThread.Name} prints {i}"); } } static void PrintNumber(int number) { Console.WriteLine(number); } class ThreadSample { private readonly int _iterations; public ThreadSample(int iterations) { _iterations = iterations; } public void CountNumbers() { for (int i = 1; i <= _iterations; i++) { Thread.Sleep(TimeSpan.FromSeconds(0.5)); Console.WriteLine($"{Thread.CurrentThread.Name} prints {i}"); } } }
運行結果以下圖所示,與預期結果相符。
在多線程的系統中,因爲CPU的時間片輪轉等線程調度算法的使用,容易出現線程安全問題。具體可參考《深刻理解計算機系統》
一書相關的章節。
在C#中lock
關鍵字是一個語法糖,它將Monitor
封裝,給object加上一個互斥鎖,從而實現代碼的線程安全,Monitor
會在下一節中介紹。
對於lock
關鍵字仍是Monitor
鎖定的對象,都必須當心選擇,不恰當的選擇可能會形成嚴重的性能問題甚至發生死鎖。如下有幾條關於選擇鎖定對象的建議。
- 同步鎖定的對象不能是值類型。由於使用值類型時會有裝箱的問題,裝箱後的就成了一個新的實例,會致使
Monitor.Enter()
和Monitor.Exit()
接收到不一樣的實例而失去關聯性- 避免鎖定
this、typeof(type)和string
。this
和typeof(type)
鎖定可能在其它不相干的代碼中會有相同的定義,致使多個同步塊互相阻塞。string
須要考慮字符串拘留的問題,若是同一個字符串常量在多個地方出現,可能引用的會是同一個實例。- 對象的選擇做用域儘量恰好達到要求,使用靜態的、私有的變量。
如下演示代碼實現了多線程狀況下的計數功能,一種實現是線程不安全的,會致使結果與預期不相符,但也有可能正確。另一種使用了lock
關鍵字進行線程同步,因此它結果是必定的。
static void Main(string[] args) { Console.WriteLine("錯誤的多線程計數方式"); var c = new Counter(); // 開啓3個線程,使用沒有同步塊的計數方式對其進行計數 var t1 = new Thread(() => TestCounter(c)); var t2 = new Thread(() => TestCounter(c)); var t3 = new Thread(() => TestCounter(c)); t1.Start(); t2.Start(); t3.Start(); t1.Join(); t2.Join(); t3.Join(); // 由於多線程 線程搶佔等緣由 其結果是不必定的 碰巧可能爲0 Console.WriteLine($"Total count: {c.Count}"); Console.WriteLine("--------------------------"); Console.WriteLine("正確的多線程計數方式"); var c1 = new CounterWithLock(); // 開啓3個線程,使用帶有lock同步塊的方式對其進行計數 t1 = new Thread(() => TestCounter(c1)); t2 = new Thread(() => TestCounter(c1)); t3 = new Thread(() => TestCounter(c1)); t1.Start(); t2.Start(); t3.Start(); t1.Join(); t2.Join(); t3.Join(); // 其結果是必定的 爲0 Console.WriteLine($"Total count: {c1.Count}"); Console.ReadLine(); } static void TestCounter(CounterBase c) { for (int i = 0; i < 100000; i++) { c.Increment(); c.Decrement(); } } // 線程不安全的計數 class Counter : CounterBase { public int Count { get; private set; } public override void Increment() { Count++; } public override void Decrement() { Count--; } } // 線程安全的計數 class CounterWithLock : CounterBase { private readonly object _syncRoot = new Object(); public int Count { get; private set; } public override void Increment() { // 使用Lock關鍵字 鎖定私有變量 lock (_syncRoot) { // 同步塊 Count++; } } public override void Decrement() { lock (_syncRoot) { Count--; } } } abstract class CounterBase { public abstract void Increment(); public abstract void Decrement(); }
運行結果以下圖所示,與預期結果相符。
Monitor
類主要用於線程同步中, lock
關鍵字是對Monitor
類的一個封裝,其封裝結構以下代碼所示。
try { Monitor.Enter(obj); dosomething(); } catch(Exception ex) { } finally { Monitor.Exit(obj); }
如下代碼演示了使用Monitor.TyeEnter()
方法避免資源死鎖和使用lock
發生資源死鎖的場景。
static void Main(string[] args) { object lock1 = new object(); object lock2 = new object(); new Thread(() => LockTooMuch(lock1, lock2)).Start(); lock (lock2) { Thread.Sleep(1000); Console.WriteLine("Monitor.TryEnter能夠不被阻塞, 在超過指定時間後返回false"); // 若是5S不能進入同步塊,那麼返回。 // 由於前面的lock鎖定了 lock2變量 而LockTooMuch()一開始鎖定了lock1 因此這個同步塊沒法獲取 lock1 而LockTooMuch方法內也不能獲取lock2 // 只能等待TryEnter超時 釋放 lock2 LockTooMuch()纔會是釋放 lock1 if (Monitor.TryEnter(lock1, TimeSpan.FromSeconds(5))) { Console.WriteLine("獲取保護資源成功"); } else { Console.WriteLine("獲取資源超時"); } } new Thread(() => LockTooMuch(lock1, lock2)).Start(); Console.WriteLine("----------------------------------"); lock (lock2) { Console.WriteLine("這裏會發生資源死鎖"); Thread.Sleep(1000); // 這裏必然會發生死鎖 // 本同步塊 鎖定了 lock2 沒法獲得 lock1 // 而 LockTooMuch 鎖定了 lock1 沒法獲得 lock2 lock (lock1) { // 該語句永遠都不會執行 Console.WriteLine("獲取保護資源成功"); } } } static void LockTooMuch(object lock1, object lock2) { lock (lock1) { Thread.Sleep(1000); lock (lock2) ; } }
運行結果以下圖所示,由於使用Monitor.TryEnter()
方法在超時之後會返回,不會阻塞線程,因此沒有發生死鎖。而第二段代碼中lock
沒有超時返回的功能,致使資源死鎖,同步塊中的代碼永遠不會被執行。
在多線程中處理異常應當使用就近原則,在哪一個線程發生異常那麼所在的代碼塊必定要有相應的異常處理。不然可能會致使程序崩潰、數據丟失。
主線程中使用try/catch
語句是不能捕獲建立線程中的異常。可是萬一遇到不可預料的異常,可經過監聽AppDomain.CurrentDomain.UnhandledException
事件來進行捕獲和異常處理。
演示代碼以下所示,異常處理 1 和 異常處理 2 能正常被執行,而異常處理 3 是無效的。
static void Main(string[] args) { // 啓動線程,線程代碼中進行異常處理 var t = new Thread(FaultyThread); t.Start(); t.Join(); // 捕獲全局異常 AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; t = new Thread(BadFaultyThread); t.Start(); t.Join(); // 線程代碼中不進行異常處理,嘗試在主線程中捕獲 AppDomain.CurrentDomain.UnhandledException -= CurrentDomain_UnhandledException; try { t = new Thread(BadFaultyThread); t.Start(); } catch (Exception ex) { // 永遠不會運行 Console.WriteLine($"異常處理 3 : {ex.Message}"); } } private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { Console.WriteLine($"異常處理 2 :{(e.ExceptionObject as Exception).Message}"); } static void BadFaultyThread() { Console.WriteLine("有異常的線程已啓動..."); Thread.Sleep(TimeSpan.FromSeconds(2)); throw new Exception("Boom!"); } static void FaultyThread() { try { Console.WriteLine("有異常的線程已啓動..."); Thread.Sleep(TimeSpan.FromSeconds(1)); throw new Exception("Boom!"); } catch (Exception ex) { Console.WriteLine($"異常處理 1 : {ex.Message}"); } }
運行結果以下圖所示,與預期結果一致。
本文主要參考瞭如下幾本書,在此對這些做者表示由衷的感謝大家提供了這麼好的資料。
- 《CLR via C#》
- 《C# in Depth Third Edition》
- 《Essential C# 6.0》
- 《Multithreading with C# Cookbook Second Edition》
線程基礎這一章節終於整理完了,是筆者學習過程當中的筆記和思考。計劃按照《Multithreading with C# Cookbook Second Edition》這本書的結構,一共更新十二個章節,先立個Flag。
源碼下載點擊連接 示例源碼下載