前言
多線程就是允許復雜的應用程序在同一時刻執行多項任務,.NET FrameWork的托管編碼環境提供了一個完整而強大的線程模型,該模型允許編程人員精確控制在一個線程中的內容,線程何時退出,以及它訪問多少數據。
本文將要介紹什么時候用到線程、如何使用、遇到的坑。
什么時候使用線程
實際上,所有的程序都是在線程中執行的,所以理解.NET 和 Windows 如何執行線程,將有助於理解程序在運行時期的執行情況,Windows Form應用程序使用事件循環線程來處理用戶界面交互,各個窗體在不同線程上執行,如果需要在Windows Forms之間交互,就需要在線程之間交互,ASP.NET 頁面在IIS 的 多線程環境中執行一一同一頁面上不同請求可以在不同線程上執行,同一頁面可以同時在多個線程中執行。在訪問ASP.NET 頁面上的共享資源時,就會出現遇到線程問題。
線程
線程技術是指開發架構將應用程序一部分分為”線程“,使線程與程序其余部分的執行順序不同,在絕大編程語言中,都有相當於Main()的方法,該方法中的每一行都要按順序執行,下一行代碼要在上一行代碼執行完成之后再執行,線程是一種特殊的狀況,是操作系統執行多任務的一部分,它允許應用程序的一部分獨立於其他對象而單獨執行,因此就脫離了正常的執行順序
進程
當啟動應用程序的時候,系統就會自動為系統分配內存和其他相關資源,內存和資源的物理分離就叫做進程,當然,應用程序可以啟動多個進程,要 記住的是"應用程序"與"進程"並不是同義詞。分配給進程的內存為其他進程分配的內存被隔離,只有所屬的那個進程才可以訪問它,所以每個進程都應有一個線程!
在Windows中,通過訪問Windows任務管理器,就可以非常直觀的查看當前運行的進程。右擊任務欄的空白選項就可以看到了。
NET和C#對線程的支持
.NET支持自由線程,所有的屬於.NET的語言都可以使用線程,包括C#和VB.NET以及F#,前面說過"進程"是一個內存和資源的一個物理獨立的部分。后來又提到,一個進程至少有一個線程。當MicroSoft設計.NET Framework時,新增了一個隔離層,稱為應用程序域或AppDomain,應用程序並不向進程那樣獨立存在,而是進程內部的一個邏輯獨立的部分。在一個進程中可以有多個程序域,這是非常不錯的優點,通常,標准的進程不是用代理,就不可以訪問其他進程的數據。使用代理非常消耗系統開銷,編碼也變得復雜,可是如我們引入程序域的概念,就可以在一個進程中啟動多個應用程序。進程所提供的隔離區可以和應用程序一起使用,線程可以跨多個應用程序來執行,還不需要消耗因線程內部的分配而帶來的系統開銷,還有一個好處是,它們提供了類型檢查功能。
Microsoft把這些應用程序域的所有功能都封裝在了一個名為System,AppDomain的類中,Microsoft.NET程序集與這些應用程序域有着非常密切的關系,只要將程序集加載到應用程序中,它就會載入AppDomain。除非特別指出,否則程序集就會被加載到調用代碼的AppDomain中。應用程序域與線程也有直接的關系,它們可以保存一個或多個線程,就像進程一樣。可是不同之處是,應用程序域可以在進程內創建,但不是創建新線程。
定義線程
了解了有關的理論和模型后,現在要介紹一些實際的代碼,下面的實例將使用AppDomain來設置數據,檢索數據以及表示AppDomain在執行的線程,創建一個appdomain.cs。
//定義應用程序域
public AppDomain Domain;
public int ThreadId; //設置值 public void SetDomainData(string vName,string vValue) { Domain.SetData(vName,(object)vValue); ThreadId = AppDomain.GetCurrentThreadId(); } //獲取值 public string GetDomainData(string name) { return (string)Domain.GetData(name); } public static void DomainMainGo() { string DataName = "MyData"; string DataValue = "Some Data to be stored"; Console.WriteLine("Retrieving current domain"); MyAppDomain Obj = new MyAppDomain(); Obj.Domain = AppDomain.CurrentDomain; Console.WriteLine("Setting domain data"); Obj.SetDomainData(DataName,DataValue); Console.WriteLine("Getting domain data"); Console.WriteLine($"The Data found for key{DataName},is{Obj.GetDomainData(DataName)},running on thread id: {Obj.ThreadId}"); }
執行程序后,得到如下結果。
這對於沒有什么經驗的C#開發人員說也很簡單,下面看一下代碼,說明發生了什么。這個類中第一段重要的代碼如下所示:
public void SetDomainData(string vName,string vValue)
{
Domain.SetData(vName,(object)vValue); ThreadId = AppDomain.GetCurrentThreadId(); }
public string GetDomainData(string name)
{
return (string)Domain.GetData(name); }
這個方法非常簡單,這里使用AppDomain類的GetData()方法,根據鍵值獲取數據。把參數從GetDomainData方法傳遞給GetData,再把該方法的結果傳遞回調用方法。
NET中的線程
剛才說了線程是什么,介紹了很多基本知識,還有一些重要的概念,那么現在說一說.NET中的線程。說到這里你一定想起來了Systeam.Threading,這里就不列出那些屬性或者方法表了,太多了。
這里介紹一個簡單的栗子,它並不適合於解釋為什么要用線程,但去除了稍后講到的所有復雜因素,創建一個新的控制台應用程序,把文件命名為simple_thread.cs。
public class simple_thread
{
public void SimpleMethod() { int i = 5; int x = 10; int result = i * x; Console.WriteLine($"This code calculated the value {result.ToString()} from threadID:{AppDomain.GetCurrentThreadId().ToString()}"); } public static void MainGo() { simple_thread simple = new simple_thread(); simple.SimpleMethod(); ThreadStart ts = new ThreadStart(simple.SimpleMethod); Thread t = new Thread(ts); t.Start(); } }
執行以上代碼,結果如下圖所示:
現在簡單解釋一下這個簡單的代碼,線程的功能都是封裝在System.Threading命名空間中,因此,必須將這個命名空間導入到項目中。一旦導入了這個命名空間,就可以建立一個能在主線程上和新工作線程上執行的方法。在第二次執行SimpleMethod方法代碼的時候,其實就是在另一個線程上執行的。
上面的示例中,我們說明不了什么,因為我們無法顯示不同的線程ID,畢竟還沒有執行多個線程。為了模擬更高的真實性,我們再創建一個類,名為:do_something_thread.cs,其定義如下:
public class DoSomethingThread
{
static void WorkerMethod() { for (int i=1;i<1000;i++) { Console.WriteLine("Worker Thread:"+i.ToString()); } } static void MainGo() { ThreadStart ts = new ThreadStart(WorkerMethod); Thread t = new Thread(ts); t.Start(); for (int i=0;i<1000;i++) { Console.WriteLine("Primary Thread"+i.ToString()); } } }
執行以上代碼,結果如下圖所示:
現在回頭看一下ThreadStart委托,使用這些委托k可以完成一些有趣的事情。比如呢,我們都做過后天權限管理,我們將要實現一個不同的角色去登錄后台那么就會有不同的場景,列入在管理員登錄后,就要運行一個后台進程,來收集報告數據。當報告完成后,ji就通知管理員,而對於普通用戶,就不需要這個功能了,這就是ThreadStart的面向對象特性。下面我們創建一個ThreadStartBranching.cs。
public class ThreadStartBranching
{
enum UserClass { ClassAdmin, ClassUser } static void AdminMethod() { Console.WriteLine("Admin Method"); } static void UserMethod() { Console.WriteLine("User Method"); } static void ExecuteFor(UserClass uc) { ThreadStart ts; ThreadStart tsAdmin = new ThreadStart(AdminMethod); ThreadStart tsUser = new ThreadStart(UserMethod); if (uc == UserClass.ClassAdmin) ts = tsAdmin; else ts = tsUser; Thread t = new Thread(ts); t.Start(); } public static void MainGo() { //excute in the context of an admin user ExecuteFor(UserClass.ClassAdmin); ExecuteFor(UserClass.ClassUser); Console.ReadLine(); } }
以上代碼的結果是非常簡單的。

線程的屬性和方法
Thread類有很多方法和屬性,使用Systeam.Threading 命名空間可以使控制線程的執行簡單得多。到目前為止,我們只是創建線程並啟動它。
下面再介紹兩個Thread類的成員:Sleep()方法和IsAlive屬性,線程何以睡眠一段時間,直到時間到了才會終端睡眠。要使線程睡眠直接Sleep方法即可。觀察下面代碼,創建一個threadSleep.cs文件。
static void WorkFunction()
{
string ThreadState; for (int i = 1;i<50000;i++) { if (i % 5000 == 0) { ThreadState = Thread.CurrentThread.ToString(); Console.WriteLine("Worker"+ThreadState); } } Console.WriteLine("Worker Function Complete"); } public static void MainGo() { string ThreadState; Thread t = new Thread(new ThreadStart(WorkFunction)); t.Start(); while (t.IsAlive) { Console.WriteLine("Still waiting.Iam going back to sleep!!"); Thread.Sleep(1); } ThreadState = t.ThreadState.ToString(); Console.WriteLine("He's finally dene! Thread state is "+ ThreadState); }
輸出結果如下:
注意:在for循環中試驗不同的值並傳遞到sleep方法中,就可以看到不同的結果。
首先,創建了一個線程,直接創建了一個ThreadStart變量當作參數。使用IsAlice屬性可以判斷線程是否還在執行,代碼的其他部分是沒有標准的,但是要注意其他的點,首先利用sleep制定線程睡眠,給其他線程讓出執行時間,傳入的參數單位是毫秒。
線程的優先級
線程的優先級決定了各個線程之間相對的優先級。ThreadPriority 枚舉定義了可用f設置線程優先級的值,可用的值是:
●Highest(最高值)
●AboveNormal(高於正常值)
●Normal(正常值)
●BelowNormal(低於正常值)
●Lowest(最低值)
當運行庫創建-個線程,但還沒有分配優先級時,線程的初始優先級為Normal。不過,叮以使用ThreadPriority枚舉改變優先級。在介紹線程優先級的例子之前,先討論一下線程的優先級。創建一個簡單的線程實例,顯示當前線程thread_ priority.cs 的名稱、狀態和優先級信息:
public class ThreadPriority
{
public static Thread worker; public static void MainGo() { Console.WriteLine("Entering void Main()"); worker = new Thread(new ThreadStart(FindPriority)); worker.Name = "FindPriority() Thread"; worker.Start(); Console.WriteLine("Exiting void Main()"); } public static void FindPriority() { Console.WriteLine("Name:"+worker.Name); Console.WriteLine("State:"+worker.ThreadState.ToString()); Console.WriteLine("Priority:"+worker.Priority.ToString()); } }
這段代碼非常簡單,定義了方法FindPriority(),該方法顯示了當前線程的名稱、狀態和優先級狀態。工作線程是以Normal優先級運行的。以下是改變優先級的代碼。
worker2.Priority = System.Threading.ThreadPriority.Highest;
需要注意的是應用程序無法限制操作系統修改由開發人員為制定線程分配的優先級,因為應用程序控制着所有線程。它們知道如何給你安排,老鐵。
計時器和回調
由於線程不像應用程序的其余代碼那樣次序運行,所以我們無法確定線程影響特定共享資源的動作,是否會在另一線程訪問共享資源之前完成。所以為了解決這些問題。使用計時器,可以按特定的時間執行方法。檢查是否完成。這是一個非常簡單的模型。但可以利用到很多情況。
計時器由兩個對象組成: TimerCallback 和Timer。TimerCallback 委托定義了以指定間隔執行的方法,而Timer對象本身就是計時器。TimerCallback 將- 一個特定的方法與計時器聯系起來。Timer的構造函數(由重載得到)需要4個參數。第一個是前面指定的TimerCallback對象,第二個是可用於將狀態傳輸給指定方法的-一個對象。后兩個參數分別是開始調用方法之后的時間,以及以后調用TimerCallback方法的時間間隔。這些參數可以是整型或者長整型,表示毫秒數。而在下面的內容中,使用的是System.TimeSpan對象,該對象可以以時鍾滴答、毫秒、秒、分、小時或天為單位指定間隔。
public class TimerExample
{
private string message;//消息
private static Timer tmr;//定時器
private static bool complete;//是否完成
}
上面的定義很簡單,將tmr聲明為靜態變量,並適用於整個類。
public class TimerExample
{
private string message;//消息
private static Timer tmr;//定時器
private static bool complete;//是否完成
public void GenerateText() { StringBuilder sb = new StringBuilder(); for (int i=1;i<200;i++) { sb.Append("This is Line"+i.ToString()+ System.Environment.NewLine); } message = sb.ToString(); } public void GetText(object state) { if (message == null) return; Console.WriteLine("message is:"+message); tmr.Dispose(); complete = true; } public void MainGo() { TimerExample obj = new TimerExample(); Thread t = new Thread(new ThreadStart(GenerateText)); t.Start(); TimerCallback timerCallback = new TimerCallback(GetText); tmr = new Timer(timerCallback,null,TimeSpan.Zero,TimeSpan.FromSeconds(2)); do { if (complete) break; } while (true); Console.WriteLine("Exiting Main."); Console.ReadLine(); } }
通過Timer定時器兩秒一次觸發,如果message還沒有設置值,那這個方法就會退出,否則就輸出了一個消息。然后由GC刪除了計時器。

線程的生命周期
當你安排一個線程的時候,這個線程會經歷幾個狀態,包括未啟動,激活,睡眠狀態,Thread類包含一些方法,允許啟動,停止,恢復,終止,掛起以及等待線程。使用線程的ThreadState可以確定線程當前的狀態。這個狀態是一個枚舉值,其定義如下。
[Flags]
public enum ThreadState { Running = 0, StopRequested = 1, SuspendRequested = 2, Background = 4, Unstarted = 8, Stopped = 16, WaitSleepJoin = 32, Suspended = 64, AbortRequested = 128, Aborted = 256 }
線程的方法
線程還四大操作,如線程睡眠,中斷線程,暫停及恢復線程,銷毀線程,連接線程。那Sleep()就不多了,當線程進入了睡眠時,他就進入了WaitSleepJoin 狀態。如果線程處於睡眠狀態,在到達指定的睡眠時間之前喚醒線程的方法,就只有Interrupt()了。這個方法會重新放到調度隊列里;下面創建一個ThreadInterupt.cs.以下是代碼:
喚醒線程
public class ThreadSleppJoin
{
public static Thread sleeper; public static Thread worker; public static void MainGo() { Console.WriteLine("Entering the void Main!"); sleeper = new Thread(new ThreadStart(SleepingThread)); worker = new Thread(new ThreadStart(AwakeTheThread)); sleeper.Start(); worker.Start(); } public static void SleepingThread() { for (int i = 1; i < 50; i++) { Console.Write(i + " "); if (i == 10 || i == 20 || i == 30) { Console.WriteLine("Going to sleep at:" + i); Thread.Sleep(20); } } } public static void AwakeTheThread() { for (int i = 50; i < 100; i++) { Console.Write(i + " "); if (sleeper.ThreadState == System.Threading.ThreadState.WaitSleepJoin) { Console.WriteLine("Interrupting the slepping thread"); sleeper.Interrupt(); } } } }
執行以上代碼結果如下
在上面的例子中,當計數器達到10、20 和30時,第一個線程(sleeper線程)進入睡眠狀態。第二個線程(worker線程)檢查第一個線程是否處於睡跟狀態。如果是,就中斷第一個線程,將其放回到調度隊列中。Interrupt(方 法是將處於睡眠狀態的線程重新激活的最好方法,如果等待資源的過程結束,且希望線程進入激活狀態,就可以使用這項功能。
暫停及恢復線程
Thread類的Supend()和Resume()方法可以用來暫停和恢復線程。Supend()方法將無限期地關閉線程,直到另一個線程將其喚醒。當調用Resume()方法時,線程會處於SuspendRequested或Suspended狀態。
public partial class Form1 : Form
{
private Thread primeNumberThread; public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { primeNumberThread = new Thread(new ThreadStart(AddItemToListBox)); primeNumberThread.Name = "Prime Numbers Example"; primeNumberThread.Priority = ThreadPriority.BelowNormal; primeNumberThread.Start(); } long num = 0; public void AddItemToListBox() { while (1==1) { if (primeNumberThread.ThreadState != System.Threading.ThreadState.Suspended || primeNumberThread.ThreadState != System.Threading.ThreadState.SuspendRequested) { Thread.Sleep(1000); num += 1; this.listBox1.Items.Add(num.ToString()); } else break; } } private void button2_Click(object sender, EventArgs e) { if (primeNumberThread.ThreadState==System.Threading.ThreadState.Running) { primeNumberThread.Suspend(); } } private void Form1_Load(object sender, EventArgs e) { this.Dispose(); } private void button3_Click(object sender, EventArgs e) { if (primeNumberThread.ThreadState != System.Threading.ThreadState.Suspended || primeNumberThread.ThreadState != System.Threading.ThreadState.SuspendRequested) { primeNumberThread.Resume(); } } }
微軟提示:不要使用Suspend和Resume方法來同步線程活動。 有沒有辦法知道當你暫停執行一個線程的哪些代碼。 如果您掛起線程安全權限評估期間持有鎖,其他線程中AppDomain可能被阻止。 如果您掛起線程執行的類構造函數時,其他線程中AppDomain中嘗試使用類被阻止。 可以很容易發生死鎖。
銷毀線程
private void button4_Click(object sender, EventArgs e)
{
primeNumberThread.Abort();
}
這樣一個線程就被銷毀了。就再也不存在了,如果還要去使用,就需要再去創建一個實例對象。
連接線程
public class Threadjoining
{
public static Thread SecondThread; public static Thread FirstThread; static void First() { for (int i=1;i<=50;i++) { Console.Write(i+" "); } } static void Second() { FirstThread.Join(); for(int i=51;i<=100;i++) { Console.Write(i+" "); } } public static void MainGo() { FirstThread = new Thread(new ThreadStart(First)); SecondThread = new Thread(new ThreadStart(Second)); FirstThread.Start(); SecondThread.Start(); } }
運行以上代碼,結果如下.

線程不是萬能的
多線程應用程序需要很多資源。線程需要內存來存儲線程本地的存儲器,所使用的線程數受到可用的內存總數限制。目前,內存是相當便宜的,因此很多計算機有很大的內存。但是,並不是所有的計算機都是這樣。如果在未知的硬件配置上運行應用程序,就不能假定應用程序有足夠的內存,也不能假定只有一個進稈會產生線程,消耗系統資源。一台計算機有很多內存空間,並不意味着所有的內存都由一個應用程序使用。
每一個線程還會導致額外的處理器開銷。如果在應用程序中創建太多的線程,就會限制線程的總執行時間。因此,與執行線程包含的指令相比,處理器在線程之間切換所花的時間會更多。如果應用程序創建了更多的線程,應用程序獲得的執行時間將比其他包含較少線程的進程更多。
最后
本文的所有相關示例,都是由我測試通過的,有問題的話,在下方留言我們一起討論。我把代碼放到了Coding上,其地址為:https://coding.net/u/zaranet/p/ThreadDemo/git/tree/master。 完結!