C#編程總結(五)多線程帶給咱們的一些思考

C#編程總結(五)多線程帶給咱們的一些思考

若有不妥之處,歡迎批評指正。html

一、何時使用多線程?

     這個問題,對於系統架構師、設計者、程序員,都是首先要面對的一個問題。程序員

     在何時使用多線程技術?sql

     在許多常見的狀況下,可使用多線程處理來顯著提升應用程序的響應能力和可用性。數據庫

     上一章,咱們講了幾個多線程的應用案例,主要的應用場景也作了介紹。這裏再也不贅述。編程

     http://www.cnblogs.com/yank/p/3232955.html安全

二、如何才能保證線程安全?

     使用多線程,這是一個必需要弄清的問題。只有瞭解了多線程對結構和程序的影響,才能真正會使用多線程,使其發揮應有的效果。服務器

     爲何應用多線程就不安全了呢?多線程

     線程安全的一個斷定指標,線程之間有沒有臨界資源,若是有臨界資源,且沒有采用合理的同步機制,就會出現多個線程競爭一個資源,如若多個線程都在爲得不到所需的資源,則會發生死鎖。死鎖,線程就會彼此僵持,系統停滯不前,若是後果嚴重,則直接致使系統崩潰。常見的案例有:生產者與消費者問題、哲學家就餐問題等。架構

      咱就根據哲學家就餐問題作個簡化:兩我的去餐館吃飯,因爲資源緊張,只有一雙筷子,每一個人都餓了,都想吃飯,且同時去搶筷子,平分秋色,兩人每人搶到一根筷子,只有使用一雙筷子才能吃飯。這時你會說了,我能夠用手抓着吃,呵呵。若是是剛出鍋的餃子,怕你抓不起來。兩我的只能面面相覷,大眼瞪小眼,就是吃不上。若是若是僵持個一年半載,都餓死了。哈哈。若是咱們給一個約定,在拿筷子時,一下拿到一雙,且吃完就交換給對方。則兩我的都高高興興吃上飯了。筷子就是臨界資源。固然,在兩我的僵持的時候,能夠進行外部干預,使得兩我的都有飯吃。好比:強制一方將筷子空閒出來,則另外一方就飯吃了。吃完了筷子空閒出來,則另外一我的也有飯吃了。異步

     只要咱們處理好臨界資源問題,也就解決了線程安全問題。

     使用多線程,未必必需要作好線程同步,可是若是有臨界資源,則必須進行線程同步處理。

三、 如何能寫出線程安全的代碼? 

     在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)
        { 
            //執行數據操做,好比新增、編輯、刪除
        }
    }
}
複製代碼

 

       可是,對於靜態變量其線程安全性是相對的,若是多個線程來修改靜態變量,這就不必定是線程安全的。而靜態方法的線程安全性,直接跟傳入的參數有關。

       總之:

      針對變量、對象、類型,說線程安全性,比較籠統,在這裏,主要是想讓你們明白,哪些地方須要注意線程安全性。對於變量、對象(屬性、方法)、靜態變量、靜態方法,其線程安全性是相對的,須要根據實際狀況而定。

     萬劍不離其宗,其斷定標準:是否有臨界資源。

 四、集合類型是線程安全的嗎?

      經常使用的集合類型有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);
                }
            }
        }
複製代碼

五、如何進行線程同步?

在第三章作了具體講解,並介紹了經常使用的幾種線程同步的方法,具體可見:

http://www.cnblogs.com/yank/p/3227324.html

六、IIS多線程應用

IIS有多個應用程序池,每一個應用程序池對應一個w3wp.exe的進程,每一個應用程序池對應多個應用程序,每一個應用程序對應一個應用程序域,應用程序域中包含了共享數據和多個線程,線程中有指定操做。由下圖咱們就能清晰的瞭解整個結構。

 

七、如何有效使用多線程

     線程能夠大大提升應用程序的可用性和性能,可是多線程也給咱們帶來一些新的挑戰,要不要使用多線程,如何使用多線程,須要根據實際狀況而定。

     1)複雜度

     使用多線程,可能使得應用程序複雜度明顯提升,特別是要處理線程同步和死鎖問題。須要仔細地評估應該在何處使用多線程和如何使用多線程,這樣就能夠得到最大的好處,而無需建立沒必要要的複雜並難於調試的應用程序。

     2)數量

     線程不易過多,線程的數量與服務器配置(多核、多處理器)、業務處理具體過程,都有直接關係。線程量過少,不能充分發揮服務器的處理能力,也不能有效改善事務的處理效率。線程量過多,須要花費大量的時間來進行線程控制,最後得不償失。能夠根據實際狀況,經過檢驗測試,設定一個特定的合理的範圍。  

      3)同步和異步調用之間的選擇
      應用程序既能夠進行同步調用,也能夠進行異步調用。同步 調用在繼續以前等待響應或返回值。若是不容許調用繼續,就說調用被阻塞 了。異步或非阻塞 調用不等待響應。異步調用是經過使用單獨的線程執行的。原始線程啓動異步調用,異步調用使用另外一個線程執行請求,而與此同時原始的線程繼續處理。

      4)前臺線程和後臺線程之間的選擇
      .NET Framework 中的全部線程都被指定爲前臺線程或後臺線程。這兩種線程惟一的區別是 — 後臺線程不會阻止進程終止。在屬於一個進程的全部前臺線程終止以後,公共語言運行庫 (CLR) 就會結束進程,從而終止仍在運行的任何後臺線程。
      在默認狀況下,經過建立並啓動新的 Thread 對象生成的全部線程都是前臺線程,而從非託管代碼進入托管執行環境中的全部線程都標記爲後臺線程。然而,經過修改 Thread.IsBackground 屬性,能夠指定一個線程是前臺線程仍是後臺線程。經過將 Thread.IsBackground 設置爲 true,能夠將一個線程指定爲後臺線程;經過將 Thread.IsBackground 設置爲 false,能夠將一個線程指定爲前臺線程。

     在大多數應用程序中,您會選擇將不一樣的線程設置成前臺線程或後臺線程。一般,應該將被動偵聽活動的線程設置爲後臺線程,而將負責發送數據的線程設置爲前臺線程,這樣,在全部的數據發送完畢以前該線程不會被終止。只有在確認線程被系統隨意終止沒有不利影響時,才應該使用後臺線程。若是線程正在執行必須完成的敏感操做或事務操做,或者須要控制關閉線程的方式以便釋放重要資源,則使用前臺線程。

八、什麼時候使用線程池(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 對象:

  • 須要具備特定優先級的任務。

  • 有可能運行很長時間的任務(這樣可能阻塞其餘任務)。

  • 須要確保只有一個線程能夠訪問特定的程序集。

  • 須要有與線程相關的穩定標識。

相關文章
相關標籤/搜索