System.Threading命名空間提供了許多類型用來構(gòu)建多線程應(yīng)用程序。比如訪問線程池、Timer類、以及大量的用來同步訪問共享資源的類型。其中最基礎(chǔ)的類型是Thread。使用該類型中定義的方法能夠在當(dāng)前應(yīng)用程序域中創(chuàng)建、掛起、停止和銷毀線程。
(通過Thread可以獲得當(dāng)前線程的統(tǒng)計信息)
以編程方式創(chuàng)建次線程
可以通過ThreadStrat委托和ParameterizedThreadStart委托執(zhí)行在次線程中執(zhí)行的方法。前者執(zhí)行一個沒有參數(shù)、無返回值的方法,局限是無法給過程傳遞參數(shù),所以這一委托通常被設(shè)計用來正在后臺運行、而沒有更多的交互作用。后者則允許包含一個System.Object類型的參數(shù)。
ThreadStrat使用
public class Printer
{
public void PrintNumbers()
{
}
}
Printer p = new Printer();
Thread backgroundThread =new Thread(new ThreadStart(p.PrintNumbers));
backgroundThread.Name = "Secondary";
backgroundThread.Start();
PParameterizedThreadStart使用
前臺線程和后臺線程
n 前臺線程:能阻止應(yīng)用程序的終結(jié)。一直到所有前臺線程終止后,CLR才能關(guān)閉應(yīng)用程序(就是卸載應(yīng)用程序域);
n 后臺線程(有時候也叫守護線程,daemon thread):被CLR認(rèn)為是程序中執(zhí)行中可以做出犧牲的途徑,在任何時候,當(dāng)應(yīng)用程序結(jié)束時(主程序結(jié)束),所有后臺線程也會被自動終止(不管是否在執(zhí)行)。
所有通過Thread.Start()方法創(chuàng)建的線程都自動式前臺線程。意味著,知道所有線程本身單元的工作都執(zhí)行完成了,應(yīng)用程序才會被下載。另外,只要把IsBackground 屬性改成true,那么該線程就變成后臺線程。
看下面的代碼:
public class Printer
{
public void PrintNumbers()
{
Console.WriteLine("-> {0} is executing PrintNumbers()",
Thread.CurrentThread.Name);
Console.Write("Your numbers: ");
for (int i = 0; i < 10; i++)
{
Console.Write("{0}, ", i);
Thread.Sleep(2000);
}
Console.WriteLine();
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***** Background Threads *****\n");
Printer p = new Printer();
Thread bgroundThread =
new Thread(new ThreadStart(p.PrintNumbers));
// 后臺線程
bgroundThread.IsBackground = true;
bgroundThread.Start();
}
}
本來,成灰應(yīng)該打印出1---9,然后才會退出。在Main()方法執(zhí)行完畢,應(yīng)用程序就會自動結(jié)束,次線程雖然還在運行,但是,設(shè)置了打印的線程為后臺線程,所以它也被結(jié)束了。看不到完整的輸出。把該語句去掉以后,可以看到完整的輸出。
并發(fā)問題
看下面的代碼:
在該程序域中的主線程產(chǎn)生了10個工作線程,每個工作線程同時執(zhí)行同一個Printer實例的PrintNumbers()方法。由于沒有預(yù)防鎖定共享資源。所以在PrintNumders()輸出到控制臺之前,調(diào)用PrintNumders()方法的線程可能會被掛起。當(dāng)每個線程都調(diào)用Printer來輸出數(shù)字的時候,線程調(diào)度器可能正在切換線程,這導(dǎo)致了不同的輸出結(jié)果。
public class Printer
{
public void PrintNumbers()
{
Console.WriteLine("-> {0} is executing PrintNumbers()",
Thread.CurrentThread.Name);
Console.Write("Your numbers: ");
for (int i = 0; i < 10; i++)
{
//線程休眠秒數(shù)
Random r = new Random();
Thread.Sleep(100 * r.Next(5));
Console.Write("{0}, ", i);
}
Console.WriteLine();
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("*****Synchronizing Threads *****\n");
Printer p = new Printer();
// 使個線程全部執(zhí)行同一個對象的統(tǒng)一方法
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++)
{
threads[i] =
new Thread(new ThreadStart(p.PrintNumbers));
threads[i].Name = string.Format("Worker thread #{0}", i);
}
// 開始每一個線程
foreach (Thread t in threads)
t.Start();
Console.ReadLine();
}
}
}
(一種可能的結(jié)果)
使用lock關(guān)鍵字
對于上面的問題,需要一種方式來通過編程控制對共享資源的同步訪問。首選的是lock關(guān)鍵字。這個關(guān)鍵字允許定義一段線程同步的代碼語句。采用這種方式,后進入的線程不會中斷當(dāng)前線程,而是停止自身的下一步執(zhí)行。Lock關(guān)鍵字需要一個標(biāo)記,即一個引用對象,線程在進入鎖定訪問的時候必須獲得這個標(biāo)記。當(dāng)試圖鎖定的是一個實例級對象的私有方法時,使用方法本身所在對象的引用就可以了。將上面的代碼,修改為:
看到,結(jié)果如下:
使用System.Threading.Monitor進行同步
Lock關(guān)鍵字實際上是和System.Threading.Monitor類一同使用時的速記符號。經(jīng)過編譯器的處理,鎖定區(qū)域?qū)嶋H上被轉(zhuǎn)換為如下內(nèi)容:
Monitor.Enter(threadLock );
try
{
//顯示線程信息
Console.WriteLine("-> {0} is executing PrintNumbers()",
Thread.CurrentThread.Name);
// 輸出數(shù)字
Console.Write("Your numbers: ");
for (int i = 0; i < 10; i++)
{
//線程休眠秒數(shù)
Random r = new Random();
Thread.Sleep(100 * r.Next(5));
Console.Write("{0}, ", i);
}
Console.WriteLine();
}
finally
{
Monitor.Exit(threadLock);
}
相比較于lock,使用System.Threading.Monitor可以有更好的控制能力。使用該類型,可以(使用Wait()方法)只是活動的線程等待一段時間,在當(dāng)前線程完成操作時,使用(Pulse()或PulseAll())通知等待中的線程。
使用System.Threading.Interlocked進行原子操作
在底層的CIL代碼,賦值和簡單的數(shù)字運算都不是原子操作。System.Threading.Interlocked類允許我們來原子型操作單個數(shù)據(jù),使用它比Monitor更簡單。
Increment 和 Decrement 方法遞增或遞減變量并將結(jié)果值存儲在單個操作中。在大多數(shù)計算機上,增加變量操作不是一個原子操作,需要執(zhí)行下列步驟:
1.將實例變量中的值加載到寄存器中。
2.增加或減少該值。
3.在實例變量中存儲該值。
如果不使用 Increment 和 Decrement,線程會在執(zhí)行完前兩個步驟后被搶先。然后由另一個線程執(zhí)行所有三個步驟。當(dāng)?shù)谝粋€線程重新開始執(zhí)行時,它覆蓋實例變量中的值,造成第二個線程執(zhí)行增減操作的結(jié)果丟失。
Exchange
{
int newVal = Interlocked.Increment(ref intVal);
}
使用
{
int newVal = Interlocked.Exchange(ref intVal,83);
}
最后一個同步原語是[Synchronization]特性。它位于System.Runtime.Remoting.Contexts命名空間下。這個類級別的特性有效地使對象的所有實例的成員都保持線程安全。當(dāng)CLR分配帶[Synchronization]對象時,它會把這個對象放在同步上下文中。(它的主要問題是,即使一個方法沒有使用線程敏感的數(shù)據(jù),CLR仍然會鎖定對該費那個發(fā)的調(diào)用,這會降低性能)。
使用Timer Callback
許多程序需要定期調(diào)用具體費那個發(fā)。比如,可能有一個應(yīng)用程序需要在狀態(tài)欄上通過一個輔助函數(shù)顯示當(dāng)前時間,或,可能希望應(yīng)用程序調(diào)用一個輔助函數(shù),讓它執(zhí)行非緊迫的后臺任務(wù),比如,監(jiān)察是否擁有新郵件。像這些情況,可以使用System.Threading.Timer和TimerCallback委托。
CLR線程池
為了提高效率,使用BeginInvoke()的時候,CLR并不會創(chuàng)建新的線程,委托的BeginInvoke()方法創(chuàng)建了維護的工作線程池??梢允褂?/font>Threading的ThreadPool類型與之交互。
如果想要使用池中的工作線程排隊執(zhí)行一個方法,可以使用ThreadPool.QueueUserWorkItem()方法。這個被重載的方法可以讓你傳遞一個可選的Object類型的自定義狀態(tài)數(shù)據(jù)給WaitCallback委托實例:
class ThreadPoolApp
{
public static void executeThreadPool()
{
Console.WriteLine("Main thread started. ThreadID = {0}",
Thread.CurrentThread.ManagedThreadId);
Printer p = new Printer();
WaitCallback workItem = new WaitCallback(PrintTheNumbers);
// 調(diào)用這個方法10次
for (int i = 0; i < 10; i++)
{
ThreadPool.QueueUserWorkItem(workItem, p);
}
Console.WriteLine("所有任務(wù)都已經(jīng)入隊,執(zhí)行");
}
static void PrintTheNumbers(object state)
{
Printer task = (Printer)state;
task.PrintNumbers();
}
}
我們可以看到,比起顯示地創(chuàng)建線程對象,使用這個被CLR鎖維護的線程池的好是:
1.線程池減少了線程創(chuàng)建、開始。亭子的次數(shù),提高了效率。
2.使用線程池,能夠使我們的注意力費那個在業(yè)務(wù)邏輯上,而不是多線程架構(gòu)上
但是,有時候,還是需要手動管理。比如:
1.如果需要設(shè)置線程優(yōu)先級別,或者線程池中的線程總是后臺線程,且它的優(yōu)先級是默認(rèn)的。
2.如果需要有一個帶有固定標(biāo)識的線程便于退出、掛起或通過名字發(fā)現(xiàn)它。
BackgroundWorker組件的作用
它位于System.ComponentModel命名空間下,構(gòu)建一個Windows Forms桌面應(yīng)用且需要執(zhí)行在應(yīng)用程序主UI線程之外的線程中長期的任務(wù)(調(diào)用遠程服務(wù)、進行數(shù)據(jù)庫事務(wù)、下載大文件等等)時,BackgroundWorker就能發(fā)揮它的所用。(可以直接使用Threading下的類型,但是BackgroundWorker更加方便)。
要使用BackgroundWorker,我們只需要告訴它希望在后臺執(zhí)行那個方法并且調(diào)用RunWorkerAsync()即可。調(diào)用線程(通常是主線程)繼續(xù)正常運行,而工作方法會一步執(zhí)行。結(jié)束以后,BackgroundWorker類型會通過觸發(fā)RunWorkerCompleted事件來通知調(diào)用線程。