前言 這倆個月沒怎麼寫文章做記錄分享,一直在忙項目上線的事情,但是學習這件事情,停下來就感覺難受,clr線程這章也是反覆看了好多遍,書讀百遍其義自見,今天我們來聊下線程基礎 1.進程是什麼,以及線程起源 2.線程開銷,以及上線文切換 3.使用線程的理由 4.線程調度和優先順序 5.前臺線程和後臺線程 ...
前言
這倆個月沒怎麼寫文章做記錄分享,一直在忙項目上線的事情,但是學習這件事情,停下來就感覺難受,clr線程這章也是反覆看了好多遍,書讀百遍其義自見,今天我們來聊下線程基礎
1.進程是什麼,以及線程起源
2.線程開銷,以及上線文切換
3.使用線程的理由
4.線程調度和優先順序
5.前臺線程和後臺線程
一、進程是什麼,以及線程起源
在電腦的早期歲月,os沒有線程的概念,整個系統只運行者一個執行線程,其中包含操作系統和應用程式的代碼。這意味著長時間運行的任務會阻止其他任務的運行。在16位windows的那些日子,列印文檔的應用程式很容易“凍結”整個機器,造成os和其他應用程式停止響應。遇到這個問題,用戶只能按Reset建或重啟電腦,所有正在運行的應用程式都會終止,造成應用程式正在處理的數據都會無端的丟失,用戶對此深惡痛絕。
Microsoft由此設計出新的OS內核,決定在一個進程中運行應用程式的每個實例,進程實際是 應用程式的實例要使用的資源的集合。每個進程都賦予了一個虛擬的地址空間,確保在一個進程中使用的代碼和數據無法由另一個進程訪問,進程也訪問不了OS的內核代碼和數據,所以,應用程式破壞不了其他應用程式或者OS自身,用戶體驗變得更好了。
聽起來不錯,但CPU本身呢?應用程式發生死迴圈會發生什麼?如果機器只有一個CPU,它會執行死迴圈,雖然數據無法被破壞,但系統仍然可能停止響應。Microsoft拿出的解決方案就是線程。作為一個Windows概念,線程的職責是對CPU進行虛擬化。Windows為每個進程都提供了該進程專用的線程(功能相當於一個CPU)。應用程式的代碼進入死迴圈,與那個代碼關聯的進程會“凍結”,但其他進程(他們有自己的線程)不會凍結,他們會繼續執行!
二、線程開銷
1.線程內核對象(thread kernel object)
OS為系統中創建的每個線程都分配並初始化這種數據結構之一。數據結構包含一組對線程進行描述的屬性。線程結構還包含所謂的線程上下文(thread context)。上下文是包含CPU寄存器集合的記憶體塊。對於x86,x64和ARM CPU架構,線程上下文分別使用約700,1240和350位元組的記憶體
2.線程環境快(thread environment block,TEB)
TEB是在用戶模式(應用程式代碼能快速訪問的地址空間)中分配和初始化的記憶體塊。TEB耗用一個記憶體頁(x86,x64,和ARM CPU中是4KB)。TEB包含線程的異常處理鏈首(head)。線程進入的每個try塊都在鏈首插入一個節點(node);線程退出try塊時從鏈中刪除該節點。此外,TEB還包含線程的“線程本地存儲”數據,以及GDI(Graphics Device Interface,圖形設備介面)和OpenGL圖形使用的一些數據結構
3.用戶模式棧(user-mode stack)
用戶模式棧存儲傳給方法的局部變數和實參。它還包含一個地址;指出當前方法返回時,線程應該從什麼地方接著執行。Windows預設為每個線程的用戶模式棧分配1MB記憶體。
4.內核模式棧(kernel-mode-stack)
應用程式代碼向操作系統中的內核模式函數傳遞實參時,還會使用內核模式棧。由於對安全的考慮,針對從用戶模式的代碼傳給內核的任何實參,Windows都會把它們從線程的用戶模式棧複製到線程的內核模式棧。一經複製,內核就可驗證實參的值。由於應用程式代碼不能訪問內核模式棧,所以應用程式無法更改驗證後的實參值。OS內核代碼開始處理複製的值。除此之外,內核會調用它自己內部的方法,並利用內核模式棧傳遞它自己的實參、存儲函數的局部變數以及存儲返回地址。
4.DLL線程連接(attach)和線程分離(detach)通知
Windows的一個策略是,任何時候在進程中創建線程,都會調用進程中載入的所有非托管DLL的DllMain方法,並向該方法傳遞DLL_THREAD_ATTACH標誌。類似地,任何時候線程終止,都會調用進程中的所有非托管DLL的DllMain方法,並向方法傳遞DLL_THREAD_DETACH標誌。有的DLL需要獲取這些通知,才能為進程中創建/銷毀的每個線程執行特殊的初始化或(資源)清理操作。
也就是說線程創建銷毀時會調用所有DLL中的這個函數,嚴重影響了進程中創建和銷毀線程的性能
上下文切換
單CPU電腦一次只能做一件事情。Windows必須在系統中的所有線程(邏輯CPU)之間共用物理CPU。
Windows任何時刻只將一個線程分配給一個CPU 。那個線程能運行一個“時間片”(有時也成為“量”或者“量程”,即quantum)的長度。時間片到期,Windows就上下文切換到另一個線程。每次上下文切換都要求windows執行以下操作。
1.將CPU寄存器的值保存到當前正在運行的線程的內核對象內部的一個上下文結構中。
2.從現有線程集合中選出一個線程供調度。
3.將所選上下文結構中的值載入到CPU的寄存器中。
上下文切換完成後,CPU執行所選的線程,直到它的時間片到期。然後發生下次上下文切換。Windows大約每30毫秒執行一次上下文切換。
要構建高性能應用程式和組件,就應該儘量避免上下文切換。
減少線程的數量也會顯著提升垃圾回收的性能。
三、使用線程的理由
1.可響應性(通常是對於客戶端GUI應用程式)
例如windows中每個進程提供它自己的線程,確保發生死迴圈的應用程式不會妨礙其它應用程式。類似地,在客戶端GUI應用程式中,可以將一些工作交給一些線程進行,使GUI線程能靈敏的響應用戶輸入。
2.性能(對於客戶端和服務端應用程式)
由於windows每個CPU調度一個線程,而且多個CPU能併發執行這些線程,所以同時執行多個操作能提升性能。
滿足以下任何條件,就可考慮顯式創建自己的線程
1.線程需要以非普通線程優先順序運行
2.需要線程表現為一個前臺線程,防止應用程式線上程結束任務前終止
3.計算限制的任務需要長時間運行
4.要啟動的線程,並可能調用Thread的abort方法來提前終止它
四、線程調度和優先順序
每個線程都分配了從0(最低)到31(最高)的優先順序。
只要存在可調度的優先順序31的線程,系統就永遠不會將優先順序0~30的任何線程分配給CPU。這種情況成為饑餓。
系統啟動時會創建一個特殊的零頁線程(zero page thread)。該線程的優先順序為0,而且是整個系統唯一優先順序為0的線程。在沒有其他線程需要“幹活兒”的時候,零頁線程將系統RAM的所有空閑頁清零。
五、前臺線程和後臺線程
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; namespace ConsoleApplication2 { class Program { static void Main(string[] args) { //創建新線程(預設為前臺線程) Thread t = new Thread(Worker); //使線程成為後臺線程 t.IsBackground = true; //啟動線程 t.Start(); //如果t是前臺線程,則應用程式大概10秒後才終止 //如果t是後臺線程,則應用程式立即終止 Console.WriteLine("Returing from Main"); } private static void Worker() { //模擬做10秒鐘的工作 Thread.Sleep(10000); //下麵這行代碼只有由一個前臺線程執行時才會顯示 Console.WriteLine("Returning from Worker"); } } }View Code
應用程式的主線程以及通過構造一個Thread對象來顯式創建的任何線程都預設為前臺線程。相反,線程池線程預設為後臺線程。
另外,由進入托管執行環境的本機(native)代碼創建的任何線程都被標記為後臺線程。
天道酬勤,大道至簡,堅持。