基本概念
什么是進程?
當一個程序開始運行時,它就是一個進程,進程包括運行中的程序和程序所使用到的內存和系統資源。一個進程至少有一個主線程。
什么是線程?
線程是程序中的一個執行流,每個線程都有自己的專有寄存器(棧指針、程序計數器等),但代碼區是共享的,即不同的線程可以執行同樣的函數。
什么是多線程?
多線程是指程序中包含多個執行流,即在一個程序中可以同時運行多個不同的線程來執行不同的任務,也就是說允許單個程序創建多個並行執行的線程來完成各自的任務。
多線程的好處?
可以提高 CPU 的利用率。在多線程程序中,一個線程必須等待的時候,CPU 可以運行其它的線程而不是等待,這樣就大大提高了程序的效率。
多線程的不利方面?
線程也是程序,所以線程需要占用內存,線程越多占用內存也越多。
多線程需要協調和管理,所以 CPU 需要花時間來跟蹤線程。
線程之間對共享資源的訪問會相互影響,必須解決競用共享資源的問題。
線程太多會導致控制太復雜,最終可能造成很多 Bug。
static void Main(string[] args)
{
Thread.CurrentThread.Name = "It's Main Thread";
Console.WriteLine(Thread.CurrentThread.Name + " [Status:" + Thread.CurrentThread.ThreadState + "]");
}
通過 Thread 類的靜態屬性 CurrentThread 可以獲取當前正在執行的線程。不管創建了多少個這個類的實例,但是類的靜態屬性在內存中只有一個。很容易理解 CurrentThread 為什么是靜態的--雖然有多個線程同時存在,但是在某一個時刻,CPU 只能執行其中一個!
在.net framework class library 中,所有與多線程機制應用相關的類都是放在 System.Threading 命名空間中。
Thread 類有幾個至關重要的方法:
Start() | 啟動線程 |
Sleep(int) | 靜態方法,暫停當前線程指定的毫秒數 |
Abort() | 通常使用該方法來終止一個線程 |
Suspend() | 該方法並不終止未完成的線程,它僅僅掛起線程,以后還可恢復 |
Resume() | 恢復被 Suspend()方法掛起的線程的執行 |
操縱一個線程
static void Main(string[] args)
{
Console.WriteLine("Thread Start/Stop/Join Sample:");
// 創建一個線程,使之執行 Beta 方法
Thread oThread = new Thread(Beta);
// 實際上,Start 方法只是通知 CPU 此線程可以被執行,但具體執行時機則由 CPU 自行決定。
oThread.Start();
while (!oThread.IsAlive)
{
Thread.Sleep(1);
}
oThread.Abort();
oThread.Join();
Console.WriteLine();
Console.WriteLine("Beta has finished");
try
{
Console.WriteLine("Try to restart the Alpha.Beta thread");
oThread.Start();
}
catch (ThreadStateException)
{
Console.WriteLine("ThreadStateException trying to restart Alpha.Beta. ");
Console.WriteLine("Expected since aborted threads cannot be restarted.");
Console.ReadLine();
}
}
public static void Beta()
{
while (true)
{
Console.WriteLine("Beta is running in its own thread.");
}
}
試圖用 Thread.Start() 方法重新啟動線程 oThread,但顯然 Abort() 方法帶來的后果是不可恢復的終止線程,所以最后程序會拋出 ThreadStateException 異常。
線程的優先級
當線程之間爭奪 CPU 時,CPU 按照線程的優先級給予服務。在 C# 應用程序中,用戶可以設定 5 個不同的優先級,由高到低分別是 Highest,AboveNormal,Normal,BelowNormal,Lowest,在創建線程時如果不指定優先級,那么系統默認為 ThreadPriority.Normal。
通過設定線程的優先級,我們可以安排一些相對重要的線程優先執行,例如對用戶的響應等等。
static void Main(string[] args)
{
Thread t1 = new Thread(() =>
{
System.Diagnostics.Stopwatch watch = new System.Diagnostics.Stopwatch();
watch.Start();
for (int i = 0; i < 20000; i++)
{
// 模擬耗時工作
var obj = new { name = "XXX", age = 37 };
GC.Collect();
}
watch.Stop();
Console.WriteLine("t1 finished[ {0} ]", watch.ElapsedMilliseconds);
});
Thread t2 = new Thread(() =>
{
System.Diagnostics.Stopwatch watch = new System.Diagnostics.Stopwatch();
watch.Start();
for (int i = 0; i < 20000; i++)
{
var obj = new { name = "XXX", age = 37 };
GC.Collect();
}
watch.Stop();
Console.WriteLine("t2 finished[ {0} ]", watch.ElapsedMilliseconds);
});
t1.Priority = ThreadPriority.AboveNormal;
t2.Priority = ThreadPriority.BelowNormal;
t1.Start();
t2.Start();
}
線程的調度算法非常復雜。記住,優先級高的線程並不一定先執行,但 CPU 會將更多的時間片分給優先級高的線程,因此,在相同任務量的前提下,高優先級線程將會較快的完成任務。
Winform 中多線程的應用
在 Winform 程序總,一般稱繪制窗體和響應用戶的線程為主線程,或 UI 線程。單線程最顯著的缺點是,當一個事件發生,程序進行一個耗時的運算動作時,UI 線程會出現假死現象,此時會無視對用戶的響應。
下面的代碼會模擬一些不同的情況:
void DoSomething()
{
for (int i = 0; i < 900000000; i++)
{
}
MessageBox.Show("It's finished.");
}
void ShowStr(object obj)
{
var list = obj as List<string>;
if (list != null)
{
foreach (var item in list)
{
MessageBox.Show(item.ToString());
}
}
else
MessageBox.Show("null");
}
// UI 單線程,運行時窗體會卡死一段時間
private void btnUI_Click(object sender, EventArgs e)
{
DoSomething();
}
// 調用無參函數,此時窗體能響應用戶
private void btnThreadA_Click(object sender, EventArgs e)
{
Thread thread = new Thread(DoSomething);
thread.Start();
}
// 當所有前台線程都關閉時,后台線程將立即結束運行,無條件的關閉
// 而前台線程運行時,即使關閉 Form 主程序,該線程仍將繼續運行,直到計算完畢
private void btnThreadB_Click(object sender, EventArgs e)
{
Thread thread = new Thread(DoSomething);
thread.IsBackground = true;
thread.Start();
}
// 調用有參函數
private void btnThreadC_Click(object sender, EventArgs e)
{
Thread thread = new Thread(ShowStr);
thread.Start(new List<string> { "Jacky", "Skysoot", "Sam" });
}
要注意的是,線程在調用有參函數時,通過 Start() 方法傳遞了參數給指定委托,該委托又將參數傳遞給了該線程欲運行的函數。看微軟 Thread 類定義的元數據:
Thread 類的 4 個構造函數基本分為 2 類,有參和無參。而 ParameterizedThreadStart 委托定義的方法原型的參數為 Object 類型,這提高了傳參最大的靈活性。當然,在被調用的函數內部,需要依據一定的約定將 Object 對象進行轉型處理。
繞圈子封裝一個線程類進行函數和參數的傳遞
// 可定義各類型委托 示例暫定一個
public delegate void Do(object obj);
public class Worker
{
Do method;
object obj;
private void Work()
{
method(obj);
}
// 創建工人線程時 new 出工人實例 並在線程上指定 Work()
public static Thread CreateWorkerThread(Do method, object obj)
{
Worker worker = new Worker();
worker.method = method;
worker.obj = obj;
Thread t = new Thread(worker.Work);
return t;
}
}
// 任務類
public class Quest
{
public static void Quest1(object obj)
{
Console.WriteLine("工人開始:" + obj.ToString() + "\r\n");
}
public static void Quest2(object obj)
{
string[] list = obj as string[];
if (obj != null)
{
foreach (var item in list)
{
Console.WriteLine("工人開始:" + item);
}
}
}
}
public class Test
{
public static void Main(string[] args)
{
Thread t1 = Worker.CreateWorkerThread(Quest.Quest1, "搬磚");
t1.Start();
Thread t2 = Worker.CreateWorkerThread(Quest.Quest2, new string[] {"唱歌", "跳舞", "打台球" });
t2.Start();
}
}
這種封裝只是一種啟發的方式而已,並非模式。但封裝委托后的好處在於,調用方可以靈活指定 Worker 類執行什么類型的任務,加工什么參數,而無需再去考慮其余事情。