操作系統通過線程對程序的執行進行管理,當操作系統運行一個程序的時候,首先,操作系統將為這個准備運行的程序分配一個進程,以管理這個程序所需要的各種資源。在這些資源之中,會包含一個稱為主線程的線程數據結構,用來管理這個程序的執行狀態。
在Windows操作系統下,線程的的數據結構包含以下內容:
1、線程的核心對象:主要包含線程當前的寄存器狀態,當操作系統調度這個線程開始運行的時候,寄存器的狀態將被加載到CPU中,重新構建線程的執行環境,當線程被調度出來的時候,最后的寄存器狀態被重新保存到這里,已備下一次執行的時候使用。
2、線程環境塊(Thread Environment Block,TED):是一塊用戶模式下的內存,包含線程的異常處理鏈的頭部。另外,線程的局部存儲數據(Thread Local Storage Data)也存在這里。
3、用戶模式的堆棧:用戶程序的局部變量和參數傳遞所使用的堆棧,默認情況下,Windows將會被分配1M的空間用於用戶模式堆棧。
4、內核模式堆棧:用於訪問操作系統時使用的堆棧。
在搶先式多任務的環境下,在一個特定的時間,CPU將一個線程調度進CPU中執行,這個線程最多將會運行一個時間片的時間長度,當時間片到期之后,操作系統將這個線程調度出CPU,將另外一個線程調度進CPU,我們通常稱這種操作為上下文切換。
在每一次的上下文切換時,Windows將執行下面的步驟:
- 將當前的CPU寄存器的值保存到當前運行的線程數據結構中,即其中的線程核心對象中。
- 選中下一個准備運行的線程,如果這個線程處於不同的進程中,那么,還必須首先切換虛擬地址空間。
- 加載准備運行線程的CPU寄存器狀態到CPU中。
公共語言運行時CLR(Common Language Runtime)是.Net程序運行的環境,它負責資源管理,並保證應用和底層操作系統之間必要的分離。
在.Net環境下,CLR中的線程需要通過操作系統的線程完成實際的工作,目前情況下,.Net直接將CLR中的線程映射到操作系統的線程進行處理和調度,所以,我們每創建一個線程將會消耗1M以上的內存空間。但未來CLR中的線程並不一定與操作系統中的線程完全對應。通過創建CLR環境下的邏輯線程,我們可能創建更加節省資源的線程,使得大量的CLR線程可以工作在少量的操作系統線程之上。
一、線程的定義
在單CPU系統的一個單位時間(time slice)內,CPU只能運行單個線程,運行順序取決於線程的優先級別。如果在單位時間內線程未能完成執行,系統就會把線程的狀態信息保持到線程的本地存儲器(TLS)中,以便下次執行時恢復執行。而多線程只是系統帶來的一個假象,它在多個單位時間內進行多個線程的切換,因為切換頻密而且單位時間非常短暫,所以多線程被視作同時運行。
適當使用多線程能提高系統的性能,比如:在系統請求大容量的數據時使用多線程,把數據輸出工作交給異步線程,使主線程保持其穩定性去處理其他問題。但需要注意一點,因為CPU需要花費不少的時間在線程的切換上,所以過多地使用多線程反而會導致性能的下降。
1、System.Threading 命名空間中的常用類
在System.Threading命名空間內提供多個方法來構建多線程應用程序,其中ThreadPool與Thread是多線程開發中最常用到的,在.NET中專門設定了一個CLR線程池專門用於管理線程的運行,這個CLR線程池正是通過ThreadPool類來管理,而Thread是管理線程的最直接方式。
類 | 說明 |
AutoResetEvent | 通知正在等待的線程已發生事件 |
ManualResetEvent | 通知正在等待的線程已發生事件 |
Interlocked | 為多個線程共享的變量提供原子操作 |
Monitor | 提供同步對對象的訪問的機制 |
Mutex | 一個同步基元,也可用於進程間同步 |
Thread | 創建並控制線程,設置其優先級並獲取其狀態 |
ThreadPool | 提供一個線程池,該線程池可用於發送工作項、處理異步 I/O、代表其他線程等待以及處理計時器 |
WaitHandle | 封裝等待對共享資源的獨占訪問的操作系統特定的對象 |
ReadWriterLock | 讀寫鎖 |
Semaphore | 控制線程的訪問數量 |
二、線程的優先級
理解有誤,示例並不正確;
為了方便線程的管理,線程有個優先級,優先級用於決定哪個線程優先執行,在Thread對象中就是Priority屬性。
優先級由低到高分別是:
優先級 | 說明 |
Lowest | 可以將 Thread 安排在具有任何其他優先級的線程之后 |
BelowNormal | 可以將 Thread 安排在具有 Normal 優先級的線程之后,在具有 Lowest 優先級的線程之前 |
Normal | 默認值。可以將 Thread 安排在具有 AboveNormal 優先級的線程之后,在具有 BelowNormal 優先級的線程之前 |
AboveNormal | 可以將 Thread 安排在具有 Highest 優先級的線程之后,在具有 Normal 優先級的線程之前 |
Highest | 可以將 Thread 安排在具有任何其他優先級的線程之前 |
先來看一個優先級的示例:
class Program { static void Main(string[] args) { //新建3個線程並設定各自的優先級 Thread t1 = new Thread(Run); t1.Priority = ThreadPriority.Lowest; Thread t2 = new Thread(Run); t2.Priority = ThreadPriority.Normal; Thread t3 = new Thread(Run); t3.Priority = ThreadPriority.Highest; //由低到高優先級的順序依次調用 t1.Start(); t2.Start(); t3.Start(); Console.ReadKey(); } public static void Run() { Console.WriteLine("我的優先級是:" + Thread.CurrentThread.Priority); } }
來看輸出:
留意到線程是按照優先級的順序執行的。
三、常用屬性
常用屬性 | 說明 |
CurrentThread | 獲取當前正在運行的線程 |
IsAlive | 獲取一個值,該值指示當前線程的執行狀態 |
IsBackground | 獲取或設置一個值,該值指示某個線程是否為后台線程, 后台線程會隨前台線程的關閉而退出 |
IsThreadPoolThread | 獲取一個值,該值指示線程是否屬於托管線程池 |
ManagedThreadId | 獲取當前托管線程的唯一標識符 |
Name | 獲取或設置線程的名稱 |
Priority | 獲取或設置一個值,該值指示線程的調度優先級 |
ThreadState | 獲取一個值,該值包含當前線程的狀態 |
ManagedThreadId是確認線程的唯一標識符,程序在大部分情況下都是通過Thread.ManagedThreadId來辨別線程的。不能夠通過Name,因為Name只是一個簡單的屬性,可以隨便改,不能保證無重復。
常用屬性示例:
class Program { static void Main(string[] args) { //新建3個線程並設定各自的優先級 Thread t1 = new Thread(Run); t1.Priority = ThreadPriority.Normal; t1.Start(); Console.ReadKey(); } public static void Run() { Thread t1 = Thread.CurrentThread; //靜態屬性,獲取當前執行這行代碼的線程 Console.WriteLine("我的優先級是:" + t1.Priority); Console.WriteLine("我是否還在執行:" + t1.IsAlive); Console.WriteLine("是否是后台線程:" + t1.IsBackground); Console.WriteLine("是否是線程池線程:" + t1.IsThreadPoolThread); Console.WriteLine("線程唯一標識符:" + t1.ManagedThreadId); Console.WriteLine("我的名稱是:" + t1.Name); Console.WriteLine("我的狀態是:" + t1.ThreadState); } }
輸出如下:
1、前台線程與后台線程的區別
我們看到上面有個屬性叫后台線程,非后台線程就叫前台線程吧,Thread.Start()啟動的線程默認為前台線程,啟動程序時創建的主線程一定是前台線程。應用程序與必須等到所有的前台線程執行完畢才會卸載。而當IsBackground設置為true時,就是后台線程了,當主線程執行完畢后就直接卸載,不再理會后台線程是否執行完畢。
前台與后台線程的設置必須在線程啟動之前進行設置,線程啟動之后就不能設置了。
Thread創建的線程是前台線程,線程池中的是后台線程。
class Program { static void Main(string[] args) { Thread t1 = new Thread(Run); t1.IsBackground = true; //設為后台線程 t1.Start(); Console.WriteLine("不等你咯,后台線程!"); //注意這里不要Console.Readxxx();,讓控制台執行完畢就自動關閉 } public static void Run() { Thread.Sleep(5000); Console.WriteLine("后台線程正在執行!"); } }
前台線程與后台線程的區別如下,上面的示例沒法用圖片來說明,簡要說發生的情況。當t1設置為前台線程時,5秒后,控制台窗口才關閉。如果t1設置為后台線程,則窗口瞬間就關閉了。
2、ThreadState的狀態
對於ThreadState的值有以下幾種:
線程狀態 | 說明 |
Aborted | 線程已停止 |
AbortRequested | 線程的Thread.Abort()方法已被調用,但是線程還未停止 |
Background | 線程在后台執行,與屬性Thread.IsBackground有關 |
Running | 線程正在正常運行 |
Stopped | 線程已經被停止 |
StopRequested | 線程正在被要求停止 |
Suspended | 線程已經被掛起(此狀態下,可以通過調用Resume()方法重新運行) |
SuspendRequested | 線程正在要求被掛起,但是未來得及響應 |
Unstarted | 未調用Thread.Start()開始線程的運行 |
WaitSleepJoin | 線程因為調用了Wait(),Sleep()或Join()等方法處於封鎖狀態 |
線程在以上幾種狀態的切換如下:
剛剛創建的線程處於已經准備好運行,但是還沒有運行的狀態,稱為Ready(准備)狀態。在操作系統的調度之下,這個線程可以進入(Runing)運行狀態。運行狀態的線程可能因為時間片用完的緣故被操作系統切換出CPU,稱為Suspended(暫停運行)狀態,也可能在時間片還沒有用完的情況下,因為等待其他優先級更高的任務,而轉換到Blocked(阻塞)狀態。在阻塞狀態下的線程,隨時可以因為再次調度而重新進入運行狀態。線程還可能通過Sleep方法進入Sleep(睡眠)狀態,當睡眠時間到期之后,可以再次被調度運行。處於運行狀態的線程還可能被主動終止執行,直接結束;也可能因為任務已經完成,被操作系統正常結束。
四、方法
方法 | 說明 |
Abort | 終止線程 |
GetDomain | 當前線程運行在的應用程序域 |
GetDomainID | 唯一的應用程序域標識符 |
Interrupt | 中斷處於 WaitSleepJoin 線程狀態的線程 |
Join | 阻塞調用線程,直到某個線程終止時為止 |
ResetAbort | 取消為當前線程請求的 Abort |
Sleep | 將當前線程阻塞指定的毫秒數 |
SpinWait | 導致線程等待由 iterations 參數定義的時間量 |
Start | 啟動線程以按計划執行 |
1、Join串行執行
Join,串行執行,相當於ajax里面的async:false
class Program { static void Main(string[] args) { Thread t1 = new Thread(Run); t1.Name = "t1"; t1.Start(); t1.Join(); //等待t1執行完之后,主線程再執行,線程間的關系為串行,非並行 Console.WriteLine("主線程執行這了么?"); Console.ReadKey(); } public static void Run() { Console.WriteLine("線程" + Thread.CurrentThread.Name + "開始執行!"); Thread.Sleep(5000); Console.WriteLine("線程" + Thread.CurrentThread.Name + "執行完畢!"); } }
輸出:
2、Interrupt 與 Abort
Interrupt和Abort:這兩個關鍵字都是用來強制終止線程,不過兩者還是有區別的。
1、Interrupt: 拋出的是 ThreadInterruptedException 異常。
Abort: 拋出的是 ThreadAbortException 異常。
2、Interrupt:如果終止工作線程,只能管到一次,工作線程的下一次sleep就管不到了,相當於一個contine操作。如果線程正在sleep狀態,則通過Interrypt跳過一次此狀態也能夠達到喚醒效果。
Abort:這個就是相當於一個break操作,工作線程徹底停止掉。 當然,你也已在catch(ThreadAbortException ex){...} 中調用Thread.ResetAbort()取消終止。
class Program { static void Main(string[] args) { Thread t1 = new Thread(Run); t1.Start(); //當Interrup時,線程已進入for循環,中斷第一次之后,第二次循環無法再停止 t1.Interrupt(); t1.Join(); Console.WriteLine("============================================================"); Thread t2 = new Thread(Run); t2.Start(); //停止1秒的目的是為了讓線程t2開始,否則t2都沒開始就直接中止了 Thread.Sleep(1000); //直接終止掉線程,線程被終止,自然無法輸出什么! t2.Abort(); Console.ReadKey(); } static void Run() { for (int i = 1; i <= 5; i++) { try { //連續睡眠5次 Thread.Sleep(2000); Console.WriteLine("第" + i + "次Sleep!"); } catch (Exception e) { Console.WriteLine("第" + i + "次Sleep被中斷!" + " " + e.Message); } } } }
輸出:
3、Suspend 與 Resume (慎用)
Thread.Suspend()與 Thread.Resume()是在Framework1.0 就已經存在的老方法了,它們分別可以掛起、恢復線程。但在Framework2.0中就已經明確排斥這兩個方法。這是因為一旦某個線程占用了已有的資源,再使用Suspend()使線程長期處於掛起狀態,當在其他線程調用這些資源的時候就會引起死鎖!所以在沒有必要的情況下應該避免使用這兩個方法。在MSDN中,這兩個方法也已被標記為已過時。
五、ThreadStart委托
ThreadStart所生成並不受線程池管理。
通過ThreadStart委托啟動線程:
class Program { static void Main(string[] args) { Console.WriteLine("主線程Id是:" + Thread.CurrentThread.ManagedThreadId); Message message = new Message(); Thread thread = new Thread(new ThreadStart(message.ShowMessage)); thread.Start(); Console.WriteLine("正在做某事......"); Console.WriteLine("主線程工作完成!"); Console.ReadKey(); } public class Message { public void ShowMessage() { string message = string.Format("異步線程Id是:{0}", Thread.CurrentThread.ManagedThreadId); Console.WriteLine(message); for (int i = 0; i < 10; i++) { Thread.Sleep(300); Console.WriteLine("異步線程當前循環執行到" + i); } } } }
輸出:
六、ParameterizedThreadStart委托
ParameterizeThreadStart委托於ThreadStart委托非常相似,但ParameterizedThreadStart委托是面向帶參數方法的。注意ParameterizedThreadStart對應方法的參數為object。
class Person { public Person(string name, int age){ this.Name = name;this.Age = age; } public string Name { get; set; } public int Age { get; set; } } class Program { static void Main(string[] args) { //整數作為參數 for (int i = 0; i < 2; i++) { Thread t = new Thread(new ParameterizedThreadStart(Run)); t.Start(i); } Console.WriteLine("主線程執行完畢!"); //自定義類型作為參數 Person p1 = new Person("關羽", 22); Person p2 = new Person("張飛", 21); Thread t1 = new Thread(new ParameterizedThreadStart(RunP)); t1.Start(p1); Thread t2 = new Thread(new ParameterizedThreadStart(RunP)); t2.Start(p2); Console.ReadKey(); } public static void Run(object i) { Thread.Sleep(50); Console.WriteLine("線程傳進來的參數是:" + i.ToString()); } public static void RunP(object o) { Thread.Sleep(50); Person p = o as Person; Console.WriteLine(p.Name + p.Age); } }
輸出:
七、TimerCallback委托
TimerCallback委托專門用於定時器的操作,這個委托允許我們定義一個定時任務,在指定的間隔之后重復調用。實際的類型與ParameterizedThreadStart委托是一樣的。
Timer類的構造函數定義如下:
public Timmer(TimerCallback callback,Object state,long dueTime,long period)
- Callback表示一個時間到達時執行的委托,這個委托代表的方法必須符合委托TimerCallback的定義。
- State表示當調用這個定時器委托時傳遞的參數。
- dutTime表示從創建定時器到第一次調用時延遲的時間,以毫秒為單位。
- Period表示定時器開始之后,每次調用之間的時間間隔,以毫秒為單位。
示例,使用TimerCallback每隔一秒鍾輸出一次時間:
class Program { static void Main(string[] args) { System.Threading.Timer clock = new System.Threading.Timer(ConsoleApplication1.Program.ShowTime, null, 0, 1000); Console.ReadKey(); } public static void ShowTime(object userData) { Console.WriteLine(DateTime.Now.ToString()); } }
輸出如下: