一、 進程 簡單來說,進程是對資源的抽象,是資源的容器,在傳統操作系統中,進程是資源分配的基本單位,而且是執行的基本單位,進程支持併發執行,因為每個進程有獨立的數據,獨立的堆棧空間。一個程式想要併發執行,開多個進程即可。 Q1:在單核下,進程之間如何同時執行? 首先要區分兩個概念——併發和並行 併發 ...
一、 進程
簡單來說,進程是對資源的抽象,是資源的容器,在傳統操作系統中,進程是資源分配的基本單位,而且是執行的基本單位,進程支持併發執行,因為每個進程有獨立的數據,獨立的堆棧空間。一個程式想要併發執行,開多個進程即可。
Q1:在單核下,進程之間如何同時執行?
首先要區分兩個概念——併發和並行
- 併發:併發是指在一段微小的時間段中,有多個程式代碼段被CPU執行,巨集觀上表現出來就是多個程式能”同時“執行。
- 並行:並行是指在一個時間點,有多個程式段代碼被CPU執行,它才是真正的同時執行。
所以應該說進程之間是併發執行。對於CPU來講,它不知道進程的存在,CPU主要與寄存器打交道。有一些常用的寄存器,如程式計數器寄存器,這個寄存器存儲了將要執行的指令的地址,這個寄存器的地址指向哪,CPU就去哪。還有一些堆棧寄存器和通用寄存器等等等,總之,這些數據構成了一個程式的執行環境,這個執行環境就叫做”上下文(Context)“,進程之間切換本質就是保存這些數據到記憶體,術語叫做”保存現場“,然後恢復某個進程的執行環境,也即是”恢復現場“,整個過程術語叫做“上下文切換”,具體點就是進程上下文切換,這就是進程之間能併發執行的本質——頻繁的切換進程上下文。這個功能是由操作系統提供的,是內核態的,對應用軟體開發人員透明。
二、 線程
進程雖然支持併發,但是對併發不是很友好,不友好是指每開啟一個進程,都要重新分配一部分資源,而線程相對進程來說,創建線程的代價比創建進程要小,所以引入線程能更好的提高併發性。在現代操作系統中,進程變成了資源分配的基本單位,而線程變成了執行的基本單位,每個線程都有獨立的堆棧空間,同一個進程的所有線程共用代碼段和地址空間等共用資源。相應的上下文切換從進程上下文切換變成了線程上下文切換。
三、 為什麼要引入進程和線程
- 提高CPU利用率,在早期的單道批處理系統中,如果執行中的代碼需要依賴與外部條件,將會導致CPU空閑,例如文件讀取,等待鍵盤信號輸入,這將浪費大量的CPU時間。引入多進程和線程可以解決CPU利用率低這個問題。
- 隔離程式之間的數據(每個進程都有單獨的地址空間),保證系統運行的穩定性。
- 提高系統的響應性和交互能力。
四、 在C#中創建托管線程
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需要掌握許多概念,下麵再說。