1、什麼時候使用多線程?
這個問題,對於系統架構師、設計者、程序員,都是首先要面對的一個問題。
在什麼時候使用多線程技術?
在許多常見的情況下,可以使用多線程處理來顯著提高應用程序的響應能力和可用性。
上一章,我們講了幾個多線程的應用案例,主要的應用場景也做了介紹。這裡不再贅述。
http://www.cnblogs.com/yank/p/3232955.html
2、如何才能保證線程安全?
使用多線程,這是一個必須要弄清的問題。只有了解了多線程對結構和程序的影響,才能真正會使用 多線程,使其發揮應有的效果。
為什麼應用多線程就不安全了呢?
線程安全的一個判定指標,線程之間有沒有臨界資源,如果有臨界資源,且沒有采用合理的同步機制 ,就會出現多個線程競爭一個資源,如若多個線程都在為得不到所需的資源,則會發生死鎖。死鎖,線 程就會彼此僵持,系統停滯不前,如果後果嚴重,則直接導致系統崩潰。常見的案例有:生產者與消費 者問題、哲學家就餐問題等。
咱就根據哲學家就餐問題做個簡化:兩個人去餐館吃飯,由於資源緊張,只有一雙筷子,每個人都餓 了,都想吃飯,且同時去搶筷子,勢均力敵,兩人每人搶到一根筷子,只有使用一雙筷子才能吃飯。這 時你會說了,我可以用手抓著吃,呵呵。如果是剛出鍋的餃子,怕你抓不起來。兩個人只能面面相觑, 大眼瞪小眼,就是吃不上。如果如果僵持個一年半載,都餓死了。哈哈。如果我們給一個約定,在拿筷 子時,一下拿到一雙,且吃完就交換給對方。則兩個人都高高興興吃上飯了。筷子就是臨界資源。當然 ,在兩個人僵持的時候,可以進行外部干預,使得兩個人都有飯吃。比如:強制一方將筷子空閒出來, 則另一方就飯吃了。吃完了筷子空閒出來,則另一個人也有飯吃了。
只要我們處理好臨界資源問題,也就解決了線程安全問題。
使用多線程,未必必須要做好線程同步,但是如果有臨界資源,則必須進行線程同步處理。
3、 如何能寫出線程安全的代碼?
在OOP中,程序員使用的無非是:變量、對象(屬性、方法)、類型等等。
1)變量
變量包括值類型和引用類型。
值類型是線程安全的,但是如果作為對象的屬性,值類型就被附加到對象上,需要參考對象的線程安 全性。
引用類型,這裡要注意的是,對於引用對象,他包括了引用和對象實例兩部分,實例需要通過對其存 儲位置的引用來訪問,對於
private Object o = new Object(),
其實可以分解為兩句話:
private Object o;
o = new Object();
其中private Object o是定義了對象的引用,也就是記錄對象實例的指針,而不是對象本身。這個引 用存儲於堆棧中,占用4個字節;當沒有使用o = new Object()時,引用本身的值為null,也就是不指向 任何有效位置;當o = new Object()後,才真正根據對象的大小,在托管堆中分配空間給對象實例,然 後將實例的指針位置賦值給前面的引用。這才完成一個對象的實例化。
引用類型的安全性,在於:可以由多個引用,同時指向一個內存地址。如果一個引用被修改,另一個 也會修改。
using System; namespace VariableSample { class Program { static void Main(string[] args) { Box b1 = new Box(); b1.Name = "BigBox"; Console.WriteLine("Create Box b1."); Console.WriteLine("Box: b1'Name is {0}.", b1.Name); Console.WriteLine("Create same Box b2."); Box b2 = b1; b2.Name = "LittleBox"; Console.WriteLine("Box: b2's Name is {0}.",b2.Name); Console.WriteLine("Box: b1's Name is {0}.", b1.Name); Console.ReadKey(); } } /// <summary> /// 盒子 /// </summary> public class Box { /// <summary> /// 名稱 /// </summary> public string Name { get; set; } } }
輸出結果:
Create Box b1.
Box: b1'Name is BigBox.
Create same Box b2.
Box: b2's Name is LittleBox.
Box: b1's Name is LittleBox.
這裡對盒子名字修改,是對兩個引用對象修改,其實我們可以將其設計為兩個多線程對對象的修改。 這裡必然存在線程安全性問題。
總之,變量的線程安全性與變量的作用域有關。
2)對象
對象是類型的實例
在創建對象時,會單獨有內存區域存儲對象的屬性和方法。所以,一個類型的多個實例,在執行時, 只要沒有靜態變量的參與,應該都是線程安全的。
這跟我們調試狀態下,是不一樣的。調試狀態下,如果多個線程都創建某實例的對象,每個對象都調 用自身方法,在調試是,會發現是訪問的同一個代碼,多個線程是有沖突的。但是,真正的運行環境是 線程安全的。
以銷售員為例,假設產品是充足的,多個銷售員,銷售產品,調用方法:Sale(),其是線程安全的。
但是,如果涉及到倉庫,必須倉庫有足夠的產品才能進行銷售,這時,多個銷售人員就有了臨界資源 :倉庫。
在這裡我們只討論對象的普通方法。至於方法傳入的參數,以及方法內對靜態變量操作的,這裡需要 根據參數和靜態變量來判定方法的線程安全性。
銷售員案例:
using System; using System.Threading; namespace MutiThreadSample.Sale { /// <summary> /// 銷售 /// </summary> public class Saler { /// <summary> /// 名稱 /// </summary> public string Name { get; set; } /// <summary> /// 間隔時間 /// </summary> public int IntervalTime { get; set; } /// <summary> /// 單位時間銷售運量 /// </summary> public int SaleAmount { get; set; } /// <summary> /// 銷售 /// </summary> public void Sale() { Console.WriteLine("銷售:{0} 於 {1} 銷售產品 {2}", this.Name, DateTime.Now.Millisecond, this.SaleAmount); Thread.Sleep(IntervalTime); } /// <summary> /// 銷售 /// </summary> /// <param name="interval">時間間隔</param> public void Sale(object obj) { WHouseThreadParameter parameter = obj as WHouseThreadParameter; if (parameter != null) { while (parameter.WHouse != null && parameter.WHouse.CanOut(this.SaleAmount)) { parameter.WHouse.Outgoing(this.SaleAmount); Console.WriteLine("Thread{0}, 銷售:{1} 於 {2} 銷售出庫產品 {3}", Thread.CurrentThread.Name, this.Name, DateTime.Now.Millisecond, this.SaleAmount); Thread.Sleep(this.IntervalTime); } } } } }
3)類型
已經講了類的實例--對象的多線程安全性問題。這裡只討論類型的靜態變量和靜態方法。
當靜態類被訪問的時候,CLR會調用類的靜態構造器(類型構造器),創建靜態類的類型對象,CLR希 望確保每個應用程序域內只執行一次類型構造器,為了做到這一點,在調用類型構造器時,CLR會為靜態 類加一個互斥的線程同步鎖,因此,如果多個線程試圖同時調用某個類型的靜態構造器時,那麼只有一 個線程可以獲得對靜態類的訪問權,其他的線程都被阻塞。第一個線程執行完 類型構造器的代碼並釋放 構造器之後,其他阻塞的線程被喚醒,然後發現構造器被執行過,因此,這些線程不再執行構造器,只 是從構造器簡單的返回。如果再一次調用這些方法,CLR就會意識到類型構造器被執行過,從而不會在被 調用。
調用類中的靜態方法,或者訪問類中的靜態成員變量,過程同上,所以說靜態類是線程安全的。
最簡單的例子,就是數據庫操作幫助類。這個類的方法和屬性是線程安全的。
using System; namespace MutiThreadSample.Static { public class SqlHelper { /// <summary> /// 數據庫連接 /// </summary> private static readonly string ConnectionString = ""; /// <summary> /// 執行數據庫命令 /// </summary> /// <param name="sql">SQL語句</param> public static void ExcuteNonQuery(string sql) { //執行數據操作,比如新增、編輯、刪除 } } }
但是,對於靜態變量其線程安全性是相對的,如果多個線程來修改靜態變量,這就不一定是線程安全 的。而靜態方法的線程安全性,直接跟傳入的參數有關。
總之:
針對變量、對象、類型,說線程安全性,比較籠統,在這裡,主要是想讓大家明白,哪些地方需要注 意線程安全性。對於變量、對象(屬性、方法)、靜態變量、靜態方法,其線程安全性是相對的,需要 根據實際情況而定。
萬劍不離其宗,其判定標准:是否有臨界資源。
4、集合類型是線程安全的嗎?
常用的集合類型有List、Dictionary、HashTable、HashMap等。在編碼中,集合應用很廣泛中,常用 集合來自定義Cache,這時候必須考慮線程同步問題。
默認情況下集合不是線程安全的。在System.Collections 命名空間中只有幾個類提供Synchronize方 法,該方法能夠超越集合創建線程安全包裝。但是,System.Collections命名空間中的所有類都提供 SyncRoot屬性,可供派生類創建自己的線程安全包裝。還提供了IsSynchronized屬性以確定集合是否是 線程安全的。但是ICollection泛型接口中不提供同步功能,非泛型接口支持這個功能。
Dictionary(MSDN解釋)
此類型的公共靜態(在 Visual Basic 中為 Shared)成員是線程安全的。 但不保證所有實例成員都 是線程安全的。
只要不修改該集合,Dictionary<TKey, TValue> 就可以同時支持多個閱讀器 。 即便如此,從頭到尾對一個集合進行枚舉本質上並不是一個線程安全的過程。 當出現枚舉與寫訪問 互相爭用這種極少發生的情況時,必須在整個枚舉過程中鎖定集合。 若允許多個線程對集合執行讀寫操 作,您必須實現自己的同步。
很多集合類型都和Dictionary類似。默認情況下是線程不安全的。當然微軟也提供了線程安全的 Hashtable.
HashTable
Hashtable 是線程安全的,可由多個讀取器線程和一個寫入線程使用。 多線程使用時,如果只有一 個線程執行寫入(更新)操作,則它是線程安全的,從而允許進行無鎖定的讀取(若編寫器序列化為 Hashtable)。 若要支持多個編寫器,如果沒有任何線程在讀取 Hashtable 對象,則對 Hashtable 的 所有操作都必須通過 Synchronized 方法返回的包裝完成。
從頭到尾對一個集合進行枚舉本質上並不是一個線程安全的過程。 即使某個集合已同步,其他線程 仍可以修改該集合,這會導致枚舉數引發異常。 若要在枚舉過程中保證線程安全,可以在整個枚舉過程 中鎖定集合,或者捕捉由於其他線程進行的更改而引發的異常。
線程安全起見請使用以下方法聲明
/// <summary>
/// Syncronized方法用來創造一個新的對象的線程安 全包裝
/// </summary>
private Hashtable hashtable = Hashtable.Synchronized(new Hashtable());
在枚舉讀取時,加lock,這裡lock其同步對象SyncRoot
/// <summary> /// 讀取 /// </summary> public void Read() { lock(hashtable.SyncRoot) { foreach (var item in hashtable.Keys) { Console.WriteLine("Key:{0}",item); } } }
查看本欄目
5、如何進行線程同步?
在第三章做了具體講解,並介紹了常用的幾種線程同步的方法,具體可見:
http://www.cnblogs.com/yank/p/3227324.html
6、IIS多線程應用
IIS有多個應用程序池,每個應用程序池對應一個w3wp.exe的進程,每個應用程序池對應多個應用程 序,每個應用程序對應一個應用程序域,應用程序域中包含了共享數據和多個線程,線程中有指定操作 。由下圖我們就能清晰的了解整個結構。
7、如何有效使用多線程?
線程可以大大提高應用程序的可用性和性能,但是多線程也給我們帶來一些新的挑戰,要不要使用多 線程,如何使用多線程,需要根據實際情況而定。
1)復雜度
使用多線程,可能使得應用程序復雜度明顯提高,特別是要處理線程同步和死鎖問題。需要仔細地評 估應該在何處使用多線程和如何使用多線程,這樣就可以獲得最大的好處,而無需創建不必要的復雜並 難於調試的應用程序。
2)數量
線程不易過多,線程的數量與服務器配置(多核、多處理器)、業務處理具體過程,都有直接關系。 線程量過少,不能充分發揮服務器的處理能力,也不能有效改善事務的處理效率。線程量過多,需要花 費大量的時間來進行線程控制,最後得不償失。可以根據實際情況, 通過檢驗測試,設定一個特定的合理的范圍。
3)同步和異步調用之間的選擇
應用程序既可以進行同步調用,也可以進行異步調用。同步 調用在繼續之前等待響應或返回值。如果不允許調用繼續,就說調用被阻塞 了。異步或非阻塞 調用不 等待響應。異步調用是通過使用單獨的線程執行的。原始線程啟動異步調用,異步調用使用另一個線程 執行請求,而與此同時原始的線程繼續處理。
4)前台線程和後台線程之間的選擇
.NET Framework 中的所有線程都被指定為前台線程或 後台線程。這兩種線程唯一的區別是 — 後台線程不會阻止進程終止。在屬於一個進程的所有前台 線程終止之後,公共語言運行庫 (CLR) 就會結束進程,從而終止仍在運行的任何後台線程。
在默認情況下,通過創建並啟動新的 Thread 對象生成的所有線程都是前台線程,而從非托管代碼進入 托管執行環境中的所有線程都標記為後台線程。然而,通過修改 Thread.IsBackground 屬性,可以指定 一個線程是前台線程還是後台線程。通過將 Thread.IsBackground 設置為 true,可以將一個線程指定 為後台線程;通過將 Thread.IsBackground 設置為 false,可以將一個線程指定為前台線程。
在大多數應用程序中,您會選擇將不同的線程設置成前台線程或後台線程。通常,應該將被動偵聽活 動的線程設置為後台線程,而將負責發送數據的線程設置為前台線程,這樣,在所有的數據發送完畢之 前該線程不會被終止。只有在確認線程被系統隨意終止沒有不利影響時,才應該使用後台線程。如果線 程正在執行必須完成的敏感操作或事務操作,或者需要控制關閉線程的方式以便釋放重要資源,則使用 前台線程。
8、何時使用線程池(ThreadPool)?
到現在為止,您可能會認識到許多應用程序都會從多線程處理中受益。然而,線程管理並不僅僅是每 次想要執行一個不同的任務就創建一個新線程的問題。有太多的線程可能會使得應用程序耗費一些不必 要的系統資源,特別是,如果有大量短期運行的操作,而所有這些操作都運行在單獨線程上。另外,顯 式地管理大量的線程可能是非常復雜的。
線程池化技術通過給應用程序提供由系統管理的輔助 線程池解決了這些問題,從而使得您可以將注意力集中在應用程序任務上而不是線程管理上。
在需要時,可以由應用程序將線程添加到線程池中。當 CLR 最初啟動時,線程池沒有包含額外的線程。 然而,當應用程序請求線程時,它們就會被動態創建並存儲在該池中。如果線程在一段時間內沒有使用 ,這些線程就可能會被處置,因此線程池是根據應用程序的要求縮小或擴大的。
注意:每個進 程都創建一個線程池,因此,如果您在同一個進程內運行幾個應用程序域,則一個應用程序域中的錯誤 可能會影響相同進程內的其他應用程序域,因為它們都使用相同的線程池。
線程池由兩種類型的線程組成:
輔助線程。輔助線程是標准系統池的一部分。它們是由 .NET Framework 管理的 標准線程,大多數功能都在它們上面執行。
完成端口線程.這種線程用於異步 I/O 操作(通過使用 IOCompletionPorts API )
對於每個計算機處理器,線程池都默認包含 25 個線程。如果所有的 25 個線程都在被使用,則附加 的請求將排入隊列,直到有一個線程變得可用為止。每個線程都使用默認堆棧大小,並按默認的優先級 運行。
下面代碼示例說明了線程池的使用。
private void ThreadPoolExample()
{
WaitCallback callback = new WaitCallback( ThreadProc );
ThreadPool.QueueUserWorkItem( callback );
}
在前面的代碼中,首先創建一個委托來引用您想要在輔助線程中執行的代碼。.NET Framework 定義 了 WaitCallback 委托,該委托引用的方法接受一個對象參數並且沒有返回值。下面的方法實現您想要 執行的代碼。
private void ThreadProc( Object stateInfo )
{
// Do something on worker thread.
}
可以將單個對象參數傳遞給 ThreadProc 方法,方法是將其指定為 QueueUserWorkItem 方法調用中 的第二個參數。在前面的示例中,沒有給 ThreadProc 方法傳遞參數,因此 stateInfo 參數為空。
在下面的情況下,使用 ThreadPool 類:
有大量小的獨立任務要在後台執行。
不需要對用來執行任務的線程進行精細控制。
Thread是顯示來管理線程。只要有可能,就應該使用 ThreadPool 類來創建線程。
在下面的情況下,使用 Thread 對象:
需要具有特定優先級的任務。
有可能運行很長時間的任務(這樣可能阻塞其他任務)。
需要確保只有一個線程可以訪問特定的程序集。
需要有與線程相關的穩定標識。