一、 行程
簡單來說,行程是對資源的抽象,是資源的容器,在傳統作業系統中,行程是資源分配的基本單位,而且是執行的基本單位,行程支持并發執行,因為每個行程有獨立的資料,獨立的堆疊空間,一個程式想要并發執行,開多個行程即可,
Q1:在單核下,行程之間如何同時執行?
首先要區分兩個概念——并發和并行
- 并發:并發是指在一段微小的時間段中,有多個程式代碼段被CPU執行,宏觀上表現出來就是多個程式能”同時“執行,
- 并行:并行是指在一個時間點,有多個程式段代碼被CPU執行,它才是真正的同時執行,
所以應該說行程之間是并發執行,對于CPU來講,它不知道行程的存在,CPU主要與暫存器打交道,有一些常用的暫存器,如程式計數器暫存器,這個暫存器存盤了將要執行的指令的地址,這個暫存器的地址指向哪,CPU就去哪,還有一些堆疊暫存器和通用暫存器等等等,總之,這些資料構成了一個程式的執行環境,這個執行環境就叫做”背景關系(Context)“,行程之間切換本質就是保存這些資料到記憶體,術語叫做”保存現場“,然后恢復某個行程的執行環境,也即是”恢復現場“,整個程序術語叫做“背景關系切換”,具體點就是行程背景關系切換,這就是行程之間能并發執行的本質——頻繁的切換行程背景關系,這個功能是由作業系統提供的,是內核態的,對應用軟體開發人員透明,
二、 執行緒
行程雖然支持并發,但是對并發不是很友好,不友好是指每開啟一個行程,都要重新分配一部分資源,而執行緒相對行程來說,創建執行緒的代價比創建行程要小,所以引入執行緒能更好的提高并發性,在現代作業系統中,行程變成了資源分配的基本單位,而執行緒變成了執行的基本單位,每個執行緒都有獨立的堆疊空間,同一個行程的所有執行緒共享代碼段和地址空間等共享資源,相應的背景關系切換從行程背景關系切換變成了執行緒背景關系切換,
三、 為什么要引入行程和執行緒
- 提高CPU利用率,在早期的單道批處理系統中,如果執行中的代碼需要依賴與外部條件,將會導致CPU空閑,例如檔案讀取,等待鍵盤信號輸入,這將浪費大量的CPU時間,引入多行程和執行緒可以解決CPU利用率低這個問題,
- 隔離程式之間的資料(每個行程都有單獨的地址空間),保證系統運行的穩定性,
- 提高系統的回應性和互動能力,
四、 在C#中創建托管執行緒
補充:之所以叫托管執行緒是因為CLR需要屏蔽作業系統執行緒的細節,這樣可以更好的跨平臺使用,所以一個托管執行緒不一定總在一個作業系統級別上的執行緒上執行,換言之一個托管執行緒可能在多個“本機執行緒上執行”,但是托管執行緒是一樣的,從托管執行緒的ID可知,
1. Thread類
在.NET中,托管執行緒分為:
- 前臺執行緒
- 后臺執行緒
一個.Net程式中,至少要有一個前臺執行緒,所有前臺執行緒結束了,所有的后臺執行緒將會被公共語言運行時(CLR)強制銷毀,程式執行結束,
如下將在控制臺程式中創建一個后臺執行緒
1 static void Main(string[] args) 2 { 3 var t = new Thread(() => 4 { 5 Thread.Sleep(1000); 6 Console.WriteLine("執行完畢"); 7 }); 8 t.IsBackground = true; 9 t.Start(); 10 }View Code

主執行緒(默認是前臺執行緒)執行完畢,程式直接退出,

但IsBackground 屬性改為false時,控制臺會列印“執行完畢”,
2. 有什么問題
直接使用Thread類來進行多執行緒編程浪費資源(服務器端更加明顯)且不方便,舉個栗子,
假如我寫一個Web服務器程序,每個請求創建一個執行緒,那么每一次我都要new一個Thread物件,然后傳入處理HttpRequest的委托,處理完之后,執行緒將會被銷毀,這將會導致浪費大量CPU時間和記憶體,在早期CPU性能不行和記憶體資源珍貴的情況下這個缺點會被放大,在現在這個缺點不是很明顯,原因是硬體上來了,
不方便體現在哪呢?
- 無法直接獲取另一個執行緒內未被捕捉的例外
- 無法直接獲取執行緒函式的回傳值
1 public static void ThrowException() 2 { 3 throw new Exception("發生例外"); 4 } 5 static void Main(string[] args) 6 { 7 var t = new Thread(() => 8 { 9 Thread.Sleep(1000); 10 ThrowException(); 11 }); 12 t.IsBackground = false; 13 try 14 { 15 t.Start(); 16 } 17 catch(Exception e) 18 { 19 Console.WriteLine(e.Message); 20 } 21 }View Code
上述代碼將會導致程式奔潰,如下圖,

要想直接獲取回傳值和可以直接從主執行緒捕捉執行緒函式內未捕捉的例外,我們可以這么做,
新建一個MyTask.cs檔案,內容如下
1 using System; 2 using System.Threading; 3 namespace ConsoleApp1 4 { 5 public class MyTask 6 { 7 private Thread _thread; 8 private Action _action; 9 private Exception _innerException; 10 public MyTask() 11 { 12 } 13 public MyTask(Action action) 14 { 15 _action = action; 16 } 17 protected virtual void Excute() 18 { 19 try 20 { 21 _action(); 22 } 23 catch(Exception e) 24 { 25 _innerException = e; 26 } 27 28 } 29 public void Start() 30 { 31 if (_thread != null) throw new InvalidOperationException("任務已經開始"); 32 _thread = new Thread(() => Excute()); 33 _thread.Start(); 34 } 35 public void Start(Action action) 36 { 37 _action = action; 38 if (_thread != null) throw new InvalidOperationException("任務已經開始"); 39 _thread = new Thread(() => Excute()); 40 _thread.Start(); 41 } 42 public void Wait() 43 { 44 _thread.Join(); 45 if (_innerException != null) throw _innerException; 46 } 47 } 48 public class MyTask<T> : MyTask 49 { 50 private Func<T> _func { get; } 51 private T _result; 52 public T Result { 53 54 private set => _result = value; 55 get 56 { 57 base.Wait(); 58 return _result; 59 } 60 } 61 public MyTask(Func<T> func) 62 { 63 _func = func; 64 } 65 public new void Start() 66 { 67 base.Start(() => 68 { 69 Result = _func(); 70 }); 71 } 72 } 73 }View Code
簡單的包裝了一下(不要在意細節),我們便可以實作我們想要的效果,
測驗代碼如下
1 public static void ThrowException() 2 { 3 throw new Exception("發生例外"); 4 } 5 public static void Test3() 6 { 7 MyTask<string> myTask = new MyTask<string>(() => 8 { 9 Thread.Sleep(1000); 10 return "執行完畢"; 11 }); 12 myTask.Start(); 13 try 14 { 15 Console.WriteLine(myTask.Result); 16 } 17 catch (Exception e) 18 { 19 Console.WriteLine(e.Message); 20 } 21 } 22 public static void Test2() 23 { 24 MyTask<string> myTask = new MyTask<string>(() => 25 { 26 Thread.Sleep(1000); 27 ThrowException(); 28 return "執行完畢"; 29 }); 30 myTask.Start(); 31 try 32 { 33 Console.WriteLine(myTask.Result); 34 } 35 catch(Exception e) 36 { 37 Console.WriteLine(e.Message); 38 } 39 } 40 public static void Test1() 41 { 42 MyTask myTask = new MyTask(() => 43 { 44 Thread.Sleep(1000); 45 ThrowException(); 46 }); 47 myTask.Start(); 48 try 49 { 50 myTask.Wait(); 51 } 52 catch (Exception e) 53 { 54 Console.WriteLine(e.Message); 55 } 56 } 57 static void Main(string[] args) 58 { 59 Test1(); 60 Test2(); 61 Test3(); 62 }

可以看到,我們可以通過簡單包裝Thread物件,便可實作如下效果
- 直接讀取執行緒函式回傳值
- 直接捕捉執行緒函式未捕捉的例外(前提是呼叫了Wait()函式或者Result屬性)
這是理解和運用Task的基礎,Task功能非常完善,但是運用好Task需要掌握許多概念,下面再說,
下一篇:(二)執行緒池與TPL
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/13480.html
標籤:C#
