.NET基礎拾遺(5)多線程開發基礎

 Index :html

 (1)類型語法、內存管理和垃圾回收基礎程序員

 (2)面向對象的實現和異常的處理基礎面試

 (3)字符串、集合與流算法

 (4)委託、事件、反射與特性數據庫

 (5)多線程開發基礎編程

 (6)ADO.NET與數據庫開發基礎數組

 (7)WebService的開發與應用基礎緩存

1、多線程編程的基本概念

  下面的一些基本概念可能和.NET的聯繫並不大,但對於掌握.NET中的多線程開發來講卻十分重要。咱們在開始嘗試多線程開發前,應該對這些基礎知識有所掌握,而且可以在操做系統層面理解多線程的運行方式。安全

1.1 操做系統層面的進程和線程

  (1)進程服務器

  進程表明了操做系統上運行着的一個應用程序。進程擁有本身的程序塊,擁有獨佔的資源和數據,而且能夠被操做系統調度。But,即便是同一個應用程序,當被強制啓動屢次時,也會被安放到不一樣的進程之中單獨運行。

  直觀地理解進程最好的方式就是經過進程管理器瀏覽,其中每條記錄就表明了一個活動着的進程:

  (2)線程

  線程有時候也被稱爲輕量級進程,它的概念和進程十分類似,是一個能夠被調度的單元,而且維護本身的堆棧和上下文環境。線程是附屬於進程的,一個進程能夠包含1個或多個線程,而且同一進程內的多個線程共享一塊內存塊和資源

  由此看來,一個線程是一個操做系統可調度的基本單元,可是它的調度受限於該線程所屬的進程,也就是說操做系統首先決定執行下一個執行的進程,進而纔會調度該進程內的線程。一個線程的基本生命週期以下圖所示:

  (3)進程和線程的區別

  最大的區別在於隔離性,每一個進程都會被單獨隔離(進程擁有本身的內存、資源和運行數據,一個進程的崩潰不會影響到其餘進程,所以進程間的交互也相對困難),而同一進程內的全部線程則共享內存和資源,而且一個線程能夠訪問和結束同一進程內的其餘線程。

1.2 多線程程序在操做系統中是並行執行的嗎?

  (1)線程的調度

  在計算機系統發展的早期,操做系統層面不存在並行的概念,全部的應用程序都在排隊等候一個單線程的隊列之中,每一個程序都必須等到前面的程序都安全執行完畢以後才能得到執行的權利,一個小小的錯誤將會致使操做系統上的全部程序的阻塞。在後來的操做系統中,逐漸產生了分時和進程、線程的概念。

  多個線程由操做系統進行調度控制,決定什麼時候運行哪一個線程。所謂線程調度,是指操做系統決定如何安排線程執行順序的算法。按常規分類,線程調度能夠分爲如下兩種:

  ①搶佔式調度

  搶佔式調度是指每一個線程都只有極少的運行時間(在Windows NT內核模式下這個時間不會超過20ms),而當時間片用完時該線程就會被強制暫停,保存上下文並把運行權利交給下一個線程。這樣調度的結果就是:全部的線程都在被不停地快速切換運行,使得用戶感受全部的線程都在並行運行

  ②非搶佔式調度

  非搶佔式調度是指某個線程在運行時不會被操做系統強制暫停,它能夠持續地運行直到運行告一段落並主動交出運行權。在這樣的調度方式之下,線程的運行就是單隊列的,而且可能產生惡意程序長期霸佔運行權的狀況。

PS:如今不少的操做系統(包括Windows在內),都同時採用了搶佔式和非搶佔式模式。對於那些優先級較高的線程,OS採用非搶佔式來給予充分的時間運行,而對於普通的線程,則採用搶佔式模式來快速地切換執行。

  (2)線程的並行問題

  在單核單CPU的硬件架構上,線程的並行運行徹底是用戶的主觀體驗。事實上,在任一時刻只可能存在一個處於運行狀態的線程。但在多CPU或多核的架構上,狀況則略有不一樣。多CPU多核的架構則容許系統徹底並行地運行兩個或多個無其餘資源爭用的線程,理論上這樣的架構可使運行性能整數倍地提升。

PS:微軟公司曾經提出超線程技術,簡單說來這是一種邏輯上模擬多CPU的技術,但實際上它們卻共享物理處理器和緩存,超線程對性能的提升至關有限。

1.3 神馬是纖程?

  (1)纖程的概念

  纖程是微軟公司在Windows上提出的一個概念,其設計目的是用來方便地移植其餘操做系統上的應用程序。一個線程能夠擁有0個或多個纖程,一個纖程能夠視爲一個輕量級的線程,它擁有本身的棧和上下文狀態。But,纖程的調度是由程序員編碼控制的,當一個纖程所在線程獲得運行時,程序員須要手動地決定運行哪個纖程

PS:事實上,Windows操做系統內核是不知道纖程的存在的,它只負責調度全部的線程,而纖程之因此成爲操做系統的概念,是由於Windows提供了關於線程操做的Win32函數,可以方便地幫助程序員進行線程編程。

  (2)纖程和線程的區別

  纖程和線程最大的區別在於:線程的調度受操做系統的管理,程序員沒法進行徹底干涉。但纖程卻徹底受控於程序員自己,容許程序員對多任務進行自定義的調度和控制,所以纖程帶給程序員很大的靈活性。

  下圖展現了進程、線程以及纖程三者之間的關係:

  (3)纖程在.NET中的地位

  須要謹記是的一點是:.NET運行框架沒有作出關於線程真實性的保證!也就是說,咱們在.NET程序中新建的線程並不必定是操做系統層面上產生的一個真正線程。在.NET框架寄宿的狀況下,一個程序中的線程極可能對應某個纖程

PS:所謂CLR寄宿,就是指CLR運行在某個應用程序而非操做系統內。常見的寄宿例子是微軟公司的SQL Server 2005。

2、.NET中的多線程編程

  .NET爲多線程編程提供了豐富的類型和機制,程序員須要作的就是掌握這些類型和機制的使用方法和運行原理。

2.1 如何在.NET程序中手動控制多個線程?

  .NET中提供了多種實現多線程程序的方法,但最直接且靈活性最大的,莫過於主動建立、運行、結束全部線程。

  (1)第一個多線程程序

  .NET提供了很是直接的控制線程類型的類型:System.Threading.Thread類。使用該類型能夠直觀地建立、控制和結束線程。下面是一個簡單的多線程程序:

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("進入多線程工做模式:");
            for (int i = 0; i < 10; i++)
            {
                Thread newThread = new Thread(Work);
                // 開啓新線程
                newThread.Start();
            }

            Console.ReadKey();
        }

        static void Work()
        {
            Console.WriteLine("線程開始");
            // 模擬作了一些工做,耗費1s時間
            Thread.Sleep(1000);
            Console.WriteLine("線程結束");
        }
    }
View Code

  在主線程中,該代碼建立了10個新的線程,這個10個線程的工做互不干擾,宏觀上來看它們應該是並行運行的,執行的結果也證明了這一點:

  

PS:這裏再次強調一點,當new了一個Thread類型對象並不意味着生成了一個線程,事實上線程的生成是在調用Thread的Start方法的時候。另外在以前的介紹中,這裏的線程並不必定是操做系統層面上產生的一個真正線程!

  (2)控制線程的狀態

  不少時候,咱們須要主動關心線程當前所處的狀態。在任意時刻,.NET中的線程都會處於以下圖所示的幾個狀態中的某一個狀態上,該圖也直觀地展現了一個線程可能通過的狀態轉換過程(該圖並無列出全部的狀態轉換途徑/緣由):

  下面的示例代碼則展現了咱們如何手動地查看和控制一個線程的狀態:

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("開始測試線程1");
            // 初始化一個線程 thread1
            Thread thread1 = new Thread(Work1);
            // 這時狀態:UnStarted
            PrintState(thread1);
            // 啓動線程
            Console.WriteLine("如今啓動線程");
            thread1.Start();
            // 這時狀態:Running
            PrintState(thread1);
            // 讓線程飛一會 3s
            Thread.Sleep(3 * 1000);
            // 讓線程掛起
            Console.WriteLine("如今掛起線程");
            thread1.Suspend();
            // 給線程足夠的時間來掛起,不然狀態多是SuspendRequested
            Thread.Sleep(1000);
            // 這時狀態:Suspend
            PrintState(thread1);
            // 繼續線程
            Console.WriteLine("如今繼續線程");
            thread1.Resume();
            // 這時狀態:Running
            PrintState(thread1);
            // 中止線程
            Console.WriteLine("如今中止線程");
            thread1.Abort();
            // 給線程足夠的時間來終止,不然的話多是AbortRequested
            Thread.Sleep(1000);
            // 這時狀態:Stopped
            PrintState(thread1);
            Console.WriteLine("------------------------------");
            Console.WriteLine("開始測試線程2");
            // 初始化一個線程 thread2
            Thread thread2 = new Thread(Work2);
            // 這時狀態:UnStarted
            PrintState(thread2);
            // 啓動線程
            thread2.Start();
            Thread.Sleep(2 * 1000);
            // 這時狀態:WaitSleepJoin
            PrintState(thread2);
            // 給線程足夠的時間結束
            Thread.Sleep(10 * 1000);
            // 這時狀態:Stopped
            PrintState(thread2);

            Console.ReadKey();
        }

        // 普通線程方法:一直在運行從未被超越
        private static void Work1()
        {
            Console.WriteLine("線程運行中...");
            // 模擬線程運行,但不改變線程狀態
            // 採用忙等狀態
            while (true) { }
        }

        // 文藝線程方法:運行10s就結束
        private static void Work2()
        {
            Console.WriteLine("線程開始睡眠:");
            // 睡眠10s
            Thread.Sleep(10 * 1000);
            Console.WriteLine("線程恢復運行");
        }

        // 打印線程的狀態
        private static void PrintState(Thread thread)
        {
            Console.WriteLine("線程的狀態是:{0}", thread.ThreadState.ToString());
        }
    }
View Code

  上述代碼的執行結果以下圖所示:

PS:爲了演示方便,上述代碼刻意地使線程處於各個狀態並打印出來。在.NET Framework 4.0 及以後的版本中,已經再也不鼓勵使用線程的掛起狀態,以及Suspend和Resume方法了。

2.2 如何使用.NET中的線程池?

  (1).NET中的線程池是神馬

  咱們都知道,線程的建立和銷燬須要很大的性能開銷,在Windows NT內核的操做系統中,每一個進程都會包含一個線程池。而在.NET中呢,也有本身的線程池,它是由CLR負責管理的。

  線程池至關於一個緩存的概念,在該池中已經存在了一些沒有被銷燬的線程,而當應用程序須要一個新的線程時,就能夠從線程池中直接獲取一個已經存在的線程。相對應的,當一個線程被使用完畢後並不會馬上被銷燬,而是放入線程池中等待下一次使用

  .NET中的線程池由CLR管理,管理的策略是靈活可變的,所以線程池中的線程數量也是可變的,使用者只需向線程池提交需求便可,下圖則直觀地展現了CLR是如何處理線程池需求的:

PS:線程池中運行的線程均爲後臺線程(即線程的 IsBackground 屬性被設爲true),所謂的後臺線程是指這些線程的運行不會阻礙應用程序的結束。相反的,應用程序的結束則必須等待全部前臺線程結束後才能退出。

  (2)在.NET中使用線程池

  在.NET中經過 System.Threading.ThreadPool 類型來提供關於線程池的操做,ThreadPool 類型提供了幾個靜態方法,來容許使用者插入一個工做線程的需求。經常使用的有如下三個靜態方法:

  ① static bool QueueUserWorkItem(WaitCallback callback)

  ② static bool QueueUserWorkItem(WaitCallback callback, Object state)

  ③ static bool UnsafeQueueUserWorkItem(WaitCallback callback, Object state)

  有了這幾個方法,咱們只須要將線程要處理的方法做爲參數傳入上述方法便可,隨後的工做都由CLR的線程池管理程序來完成。其中,WaitCallback 是一個委託類型,該委託方法接受一個Object類型的參數,而且沒有返回值。下面的代碼展現瞭如何使用線程池來編寫多線程的程序:

    class Program
    {
        static void Main(string[] args)
        {
            string taskInfo = "運行10秒";
            // 插入一個新的請求到線程池
            bool result = ThreadPool.QueueUserWorkItem(DoWork, taskInfo);
            // 分配線程有可能會失敗
            if (!result)
            {
                Console.WriteLine("分配線程失敗");
            }
            else
            {
                Console.WriteLine("按回車鍵結束程序");
            }

            Console.ReadKey();
        }

        private static void DoWork(object state)
        {
            // 模擬作了一些操做,耗時10s
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine("工做者線程的任務是:{0}", state);
                Thread.Sleep(1000);
            }
        }
    }
View Code

  上述代碼執行後,若是不輸入任何字符,那麼會獲得以下圖所示的執行結果:

PS:事實上,UnsafeQueueWorkItem方法實現了徹底相同的功能,兩者的差異在於UnsafeQueueWorkItem方法不會將調用線程的堆棧傳遞給輔助線程,這就意味着主線程的權限限制不會傳遞給輔助線程。UnsafeQueueWorkItem因爲不進行這樣的傳遞,所以會獲得更高的運行效率,可是潛在地提高了輔助線程的權限,也就有可能會成爲一個潛在的安全漏洞。

2.3 如何查看和設置線程池的上下限?

  線程池的線程數是有限制的,一般狀況下,咱們無需修改默認的配置。但在一些場合,咱們可能須要瞭解線程池的上下限和剩餘的線程數。線程池做爲一個緩衝池,有着其上下限。在一般狀況下,當線程池中的線程數小於線程池設置的下限時,線程池會設法建立新的線程,而當線程池中的線程數大於線程池設置的上限時,線程池將銷燬多餘的線程

PS:在.NET Framework 4.0中,每一個CPU默認的工做者線程數量最大值爲250個,最小值爲2個。而IO線程的默認最大值爲1000個,最小值爲2個。

  在.NET中,經過 ThreadPool 類型提供的5個靜態方法能夠獲取和設置線程池的上限和下限,同時它還額外地提供了一個方法來讓程序員獲知當前可用的線程數量,下面是這五個方法的簽名:

  ① static void GetMaxThreads(out int workerThreads, out int completionPortThreads)

  ② static void GetMinThreads(out int workerThreads, out int completionPortThreads)

  ③ static bool SetMaxThreads(int workerThreads, int completionPortThreads)

  ④ static bool SetMinThreads(int workerThreads, int completionPortThreads)

  ⑤ static void GetAvailableThreads(out int workerThreads, out int completionPortThreads)

  下面的代碼示例演示瞭如何查詢線程池的上下限閾值和可用線程數量:

    class Program
    {
        static void Main(string[] args)
        {
            // 打印閾值和可用數量
            GetLimitation();
            GetAvailable();

            // 使用掉其中三個線程
            Console.WriteLine("此處申請使用3個線程...");
            ThreadPool.QueueUserWorkItem(Work);
            ThreadPool.QueueUserWorkItem(Work);
            ThreadPool.QueueUserWorkItem(Work);

            Thread.Sleep(1000);

            // 打印閾值和可用數量
            GetLimitation();
            GetAvailable();
            // 設置最小值
            Console.WriteLine("此處修改了線程池的最小線程數量");
            ThreadPool.SetMinThreads(10, 10);
            // 打印閾值
            GetLimitation();

            Console.ReadKey();
        }


        // 運行10s的方法
        private static void Work(object o)
        {
            Thread.Sleep(10 * 1000);
        }

        // 打印線程池的上下限閾值
        private static void GetLimitation()
        {
            int maxWork, minWork, maxIO, minIO;
            // 獲得閾值上限
            ThreadPool.GetMaxThreads(out maxWork, out maxIO);
            // 獲得閾值下限
            ThreadPool.GetMinThreads(out minWork, out minIO);
            // 打印閾值上限
            Console.WriteLine("線程池最多有{0}個工做者線程,{1}個IO線程", maxWork.ToString(), maxIO.ToString());
            // 打印閾值下限
            Console.WriteLine("線程池最少有{0}個工做者線程,{1}個IO線程", minWork.ToString(), minIO.ToString());
            Console.WriteLine("------------------------------------");
        }

        // 打印可用線程數量
        private static void GetAvailable()
        {
            int remainWork, remainIO;
            // 獲得當前可用線程數量
            ThreadPool.GetAvailableThreads(out remainWork, out remainIO);
            // 打印可用線程數量
            Console.WriteLine("線程池中當前有{0}個工做者線程可用,{1}個IO線程可用", remainWork.ToString(), remainIO.ToString());
            Console.WriteLine("------------------------------------");
        }
    }
View Code

  該實例的執行結果以下圖所示:

PS:上面代碼示例在不一樣的計算機上運行可能會獲得不一樣的結果,線程池中的可用數碼不會再初始時達到最大值,事實上CLR會嘗試以必定的時間間隔來逐一地建立新線程,但這個時間間隔很是短。

2.4 如何定義線程獨享的全局數據?

  線程和進程最大的一個區別就在於線程間能夠共享數據和資源,而進程則充分地隔離。在不少場合,即便同一進程的多個線程之間擁有相同的內存空間,也須要在邏輯上爲某些線程分配獨享的數據。例如,在實際開發中每每會針對一些ORM如EF一類的上下文實體作線程內惟一實例的設置,這時就須要用到下面提到的技術。

  (1)線程本地存儲(Thread Local Storage,TLS)

  不少時候,程序員可能會但願擁有線程內可見的變量,而不但願其餘線程對其進行訪問和修改(傳統方式中的靜態變量是對整個應用程序域可見的),這就須要用到TLS的概念。所謂的線程本地存儲(TLS)是指存儲在線程環境塊內的一個結構,用來存放該線程內獨享的數據。進程內的線程不能訪問不屬於本身的TLS,這就保證了TLS內的數據在線程內是全局共享的,而對於線程外確實不可見的

  (2)定義和使用TLS變量

  在.NET中提供了下列連個方法來存取線程獨享的數據,它們都定義在System.Threading.Thread類型中:

  ① object GetData(LocalDataStoreSlot slot)

  ② void SetData(LocalDataStoreSlot slot, object data)

  下面的代碼示例則展現了這個機制的使用方法:

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("開始測試數據插槽:");
            // 建立五個線程來同時運行,可是這裏不適合用線程池,
            // 由於線程池內的線程會被反覆使用致使線程ID一致
            for (int i = 0; i < 5; i++)
            {
                Thread thread = new Thread(ThreadDataSlot.Work);
                thread.Start();
            }

            Console.ReadKey();
        }
    }

    /// <summary>
    /// 包含線程方法和數據插槽
    /// </summary>
    public class ThreadDataSlot
    {
        // 分配一個數據插槽,注意插槽自己是全局可見的,由於這裏的分配是在全部線程
        // 的TLS內建立數據塊
        private static LocalDataStoreSlot localSlot = Thread.AllocateDataSlot();

        // 線程要執行的方法,操做數據插槽來存放數據
        public static void Work()
        {
            // 將線程ID註冊到數據插槽中,一個應用程序內線程ID不會重複
            Thread.SetData(localSlot, Thread.CurrentThread.ManagedThreadId);
            // 查看一下剛剛插入的數據
            Console.WriteLine("線程{0}內的數據是:{1}",Thread.CurrentThread.ManagedThreadId.ToString(),Thread.GetData(localSlot).ToString());
            // 這裏線程休眠1秒
            Thread.Sleep(1000);
            // 查看其餘線程的運行是否干擾了當前線程數據插槽內的數據
            Console.WriteLine("線程{0}內的數據是:{1}", Thread.CurrentThread.ManagedThreadId.ToString(), Thread.GetData(localSlot).ToString());
        }
    }
View Code

  該實例的執行結果以下圖所示,從下圖能夠看出多線程的並行運行並無破壞每一個線程插槽內的數據,這就是TLS所提供的功能。

      

PS:LocalDataStoreSlot對象自己並非線程共享的,初始化一個LocalDataStoreSlot對象意味着在應用程序域內的每一個線程上都分配了一個數據插槽。

  (3)ThreadStaticAttribute特性的使用

  除了使用上面說到的數據槽以外,咱們還有另外一種方式,即ThreadStaticAttribute特性。申明瞭該特性的變量,會被.NET做爲線程獨享的數據來使用。咱們能夠將其理解爲一種被.NET封裝了的TLS機制,本質上,它仍然使用了線程環境塊來存放數據

  下面的示例代碼展現了ThreadStaticAttribute特性的使用:

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("開始測試數據插槽:");
            // 建立五個線程來同時運行,可是這裏不適合用線程池,
            // 由於線程池內的線程會被反覆使用致使線程ID一致
            for (int i = 0; i < 5; i++)
            {
                Thread thread = new Thread(ThreadStatic.Work);
                thread.Start();
            }

            Console.ReadKey();
        }
    }

    /// <summary>
    /// 包含線程靜態數據
    /// </summary>
    public class ThreadStatic
    {
        // 值類型的線程靜態數據
        [ThreadStatic]
        private static int threadId = 0;
        // 引用類型的線程靜態數據
        private static Ref refThreadId = new Ref();

        /// <summary>
        /// 線程執行的方法,操做線程靜態數據
        /// </summary>
        public static void Work()
        {
            // 存儲線程ID,一個應用程序域內線程ID不會重複
            threadId = Thread.CurrentThread.ManagedThreadId;
            refThreadId.Id = Thread.CurrentThread.ManagedThreadId;
            // 查看一下剛剛插入的數據
            Console.WriteLine("[線程{0}]:線程靜態值變量:{1},線程靜態引用變量:{2}", Thread.CurrentThread.ManagedThreadId.ToString(), threadId, refThreadId.Id.ToString());
            // 睡眠1s
            Thread.Sleep(1000);
            // 查看其餘線程的運行是否干擾了當前線程靜態數據
            Console.WriteLine("[線程{0}]:線程靜態值變量:{1},線程靜態引用變量:{2}", Thread.CurrentThread.ManagedThreadId.ToString(), threadId, refThreadId.Id.ToString());
        }
    }

    /// <summary>
    /// 簡單引用類型
    /// </summary>
    public class Ref
    {
        private int id;

        public int Id
        {
            get
            {
                return id;
            }
            set
            {
                id = value;
            }
        }
    }
View Code

  該實例的執行結果以下圖所示,正如咱們所看到的,對於使用了ThreadStatic特性的字段,.NET會將其做爲線程獨享的數據來處理,當某個線程對一個使用了ThreadStatic特性的字段進行賦值後,這個值只有這個線程本身能夠看到並訪問修改,該值對於其餘線程時不可見的。相反,沒有標記該特性的,則會被多個線程所共享。

  

2.5 如何使用異步模式讀取一個文件?

  異步模式是在處理流類型時常常採用的一種方式,其應用的領域至關廣闊,包括讀寫文件、網絡傳輸、讀寫數據庫,甚至能夠採用異步模式來作任何計算工做。相對於手動編寫線程代碼,異步模式是一個高效的編程模式。

  (1)所謂異步模式是個什麼鬼?

  所謂的異步模式,是指在啓動一個操做以後能夠繼續執行其餘工做而不會發生阻塞。以讀取文件爲例,在同步模式下,當程序執行到Read方法時,須要等到讀取動做結束後才能繼續往下執行。而異步模式則能夠簡單地通知開始讀取任務以後,繼續其餘的操做。 異步模式的優勢就在於不須要使當前線程等待,而能夠充分地利用CPU時間。

PS:異步模式區別於線程池機制的地方在於其容許程序查看操做的執行狀態,而若是利用線程池的後臺線程,則沒法確切地知道操做的進行狀態以及其是否已經結束。

  使用異步模式能夠經過一些異步彙集技巧來查看異步操做的結果,所謂的彙集技巧是指查看操做是否結束的方法,經常使用的方式是:在調用BeingXXX方法時傳入操做結束後須要執行的方法(又稱爲回調方法),同時把執行異步操做的對象傳入以便執行EndXXX方法

  (2)使用異步模式讀取一個文件

  下面的示例代碼中:

  ① 主線程中負責開始異步讀取並傳入彙集時須要使用的方法和狀態對象:

    partial class Program
    {
        // 測試文件
        private const string testFile = @"C:\AsyncReadTest.txt";
        private const int bufferSize = 1024;

        static void Main(string[] args)
        {
            // 刪除已存在文件
            if (File.Exists(testFile))
            {
                File.Delete(testFile);
            }

            // 寫入一些東西以便後面讀取
            using (FileStream stream = File.Create(testFile))
            {
                string content = "我是文件具體內容,我是否是帥得掉渣?";
                byte[] contentByte = Encoding.UTF8.GetBytes(content);
                stream.Write(contentByte, 0, contentByte.Length);
            }

            // 開始異步讀取文件具體內容
            using (FileStream stream = new FileStream(testFile, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize, FileOptions.Asynchronous))
            {
                byte[] data = new byte[bufferSize];
                // 將自定義類型對象實例做爲參數
                ReadFileClass rfc = new ReadFileClass(stream, data);
                // 開始異步讀取
                IAsyncResult result = stream.BeginRead(data, 0, data.Length, FinshCallBack, rfc);
                // 模擬作了一些其餘的操做
                Thread.Sleep(3 * 1000);
                Console.WriteLine("主線程執行完畢,按回車鍵退出程序");
            }

            Console.ReadKey();
        }
    }
View Code

  ② 定義了完成異步操做讀取以後須要調用的方法,其邏輯是簡單地打印出文件的內容:

    partial class Program
    {
        /// <summary>
        /// 完成異步操做後的回調方法
        /// </summary>
        /// <param name="result">狀態對象</param>
        private static void FinshCallBack(IAsyncResult result)
        {
            ReadFileClass rfc = result.AsyncState as ReadFileClass;
            if (rfc != null)
            {
                // 必須的步驟:讓異步讀取佔用的資源被釋放掉
                int length = rfc.stream.EndRead(result);
                // 獲取讀取到的文件內容
                byte[] fileData = new byte[length];
                Array.Copy(rfc.data, 0, fileData, 0, fileData.Length);
                string content = Encoding.UTF8.GetString(fileData);
                // 打印讀取到的文件基本信息
                Console.WriteLine("讀取文件結束:文件長度爲[{0}],文件內容爲[{1}]", length.ToString(), content);
            }
        }
    }
View Code

  ③ 定義了做爲狀態對象傳遞的類型,這個類型對全部須要傳遞的數據包進行打包:

    /// <summary>
    /// 傳遞給異步操做的回調方法
    /// </summary>
    public class ReadFileClass
    {
        // 以便回調方法中釋放異步讀取的文件流
        public FileStream stream;
        // 文件內容
        public byte[] data;

        public ReadFileClass(FileStream stream,byte[] data)
        {
            this.stream = stream;
            this.data = data;
        }
    }
View Code

  下圖展現了該實例的執行結果:

  如上面的實例,使用回調方法的異步模式須要花費一點額外的代碼量,由於它須要將異步操做的對象及操做的結果數據都打包到一個類型裏以便可以傳遞迴給回調的委託方法,這樣在委託方法中才可以有機會處理操做的結果,而且調用EndXXX方法以釋放資源。

2.6 如何阻止線程執行上下文的傳遞?

  (1)何爲線程的執行上下文

  在.NET中,每個線程都會包含一個執行上下文,執行上下文是指線程運行中某時刻的上下文概念,相似於一個動態過程的快照(SnapShot)。在.NET中,System.Threading中的ExecutionContext類型表明了一個執行上下文,該執行上下文會包含:安全上下文、調用上下文、本地化上下文、事務上下文和CLR宿主上下文等等。一般狀況下,咱們將全部這些綜合成爲線程的上下文。

  (2)執行上下文的流動

  當程序中新建一個線程時,執行上下文會自動地從當前線程流入到新建的線程之中,這樣作能夠保證新建的線程天生就就有和主線程相同的安全設置和文化等設置。下面的示例代碼經過修改安全上下文來展現線程上下文的流動性,主要使用到ExecutionContext類的Capture方法來捕獲當前想成的執行上下文。

  ① 首先定義一些輔助犯法,封裝了文件的建立、刪除和文件訪問權限檢查:

    partial class Program
    {
        private static void CreateTestFile()
        {
            if (!File.Exists(testFile))
            {
                FileStream stream = File.Create(testFile);
                stream.Dispose();
            }
        }

        private static void DeleteTestFile()
        {
            if (File.Exists(testFile))
            {
                File.Delete(testFile);
            }
        }

        // 嘗試訪問測試文件來測試安全上下文
        private static void JudgePermission(object state)
        {
            try
            {
                // 嘗試訪問文件
                File.GetCreationTime(testFile);
                // 若是沒有異常則測試經過
                Console.WriteLine("權限測試經過");
            }
            catch (SecurityException)
            {
                // 若是出現異常則測試經過
                Console.WriteLine("權限測試沒有經過");
            }
            finally
            {
                Console.WriteLine("------------------------");
            }
        }
    }
View Code

  ② 其次在入口方法中使主線程和建立的子線程訪問指定文件來查看權限上下文流動到子線程中的狀況:(這裏須要注意的是因爲在.NET 4.0及以上版本中FileIOPermission的Deny方法已過期,爲了方便測試,將程序的.NET版本調整爲了3.5)

    partial class Program
    {
        private const string testFile = @"C:\TestContext.txt";

        static void Main(string[] args)
        {
            try
            {
                CreateTestFile();
                // 測試當前線程的安全上下文
                Console.WriteLine("主線程權限測試:");
                JudgePermission(null);
                // 建立一個子線程 subThread1
                Console.WriteLine("子線程權限測試:");
                Thread subThread1 = new Thread(JudgePermission);
                subThread1.Start();
                subThread1.Join();
                // 如今修改安全上下文,阻止文件訪問
                FileIOPermission fip = new FileIOPermission(FileIOPermissionAccess.AllAccess, testFile);
                fip.Deny();
                Console.WriteLine("已成功阻止文件訪問");
                // 測試當前線程的安全上下文
                Console.WriteLine("主線程權限測試:");
                JudgePermission(null);
                // 建立一個子線程 subThread2
                Console.WriteLine("子線程權限測試:");
                Thread subThread2 = new Thread(JudgePermission);
                subThread2.Start();
                subThread2.Join();
                // 如今修改安全上下文,容許文件訪問
                SecurityPermission.RevertDeny();
                Console.WriteLine("已成功恢復文件訪問");
                // 測試當前線程安全上下文
                Console.WriteLine("主線程權限測試:");
                JudgePermission(null);
                // 建立一個子線程 subThread3
                Console.WriteLine("子線程權限測試:");
                Thread subThread3 = new Thread(JudgePermission);
                subThread3.Start();
                subThread3.Join();

                Console.ReadKey();
            }
            finally
            {
                DeleteTestFile();
            }
        }
    }
View Code

  該實例的執行結果以下圖所示,從圖中能夠看出程序中經過FileIOPermission對象來控制對主線程對文件的訪問權限,而且經過新建子線程來查看主線程的安全上下文的改變是否會影響到子線程。

      

  正如剛剛說到,主線程的安全上下文將做爲執行上下文的一部分由主線程傳遞給子線程。

  (3)阻止上下文的流動

  有的時候,系統須要子線程擁有新的上下文。拋開功能上的需求,執行上下文的流動確實使得程序的執行效率降低不少,線程上下文的包裝是一個成本較高的工做,而有的時候這樣的包裝並非必須的。在這種狀況下,咱們若是須要手動地防止線程上下文的流動,經常使用的有下列兩種方法:

  ① System.Threading.ThreadPool類中的UnsafeQueueUserWorkItem方法

  ② ExecutionContext類中的SuppressFlow方法

  下面的代碼示例展現瞭如何使用上面兩種方法阻止執行上下文的流動:

    partial class Program
    {
        private const string testFile = @"C:\TestContext.txt";

        static void Main(string[] args)
        {
            try
            {
                CreateTestFile();
                // 如今修改安全上下文,阻止文件訪問
                FileIOPermission fip = new FileIOPermission(FileIOPermissionAccess.AllAccess, testFile);
                fip.Deny();
                Console.WriteLine("已成功阻止文件訪問");
                // 主線程權限測試
                Console.WriteLine("主線程權限測試:");
                JudgePermission(null);
                // 使用UnsafeQueueUserWorkItem方法建立一個子線程
                Console.WriteLine("子線程權限測試:");
                ThreadPool.UnsafeQueueUserWorkItem(JudgePermission, null);

                Thread.Sleep(1000);

                // 使用SuppressFlow方法
                using (var afc = ExecutionContext.SuppressFlow())
                {
                    // 測試當前線程安全上下文
                    Console.WriteLine("主線程權限測試:");
                    JudgePermission(null);
                    // 建立一個子線程 subThread1
                    Console.WriteLine("子線程權限測試:");
                    Thread subThread1 = new Thread(JudgePermission);
                    subThread1.Start();
                    subThread1.Join();
                }

                // 如今修改安全上下文,容許文件訪問
                SecurityPermission.RevertDeny();
                Console.WriteLine("已成功恢復文件訪問");
                // 測試當前線程安全上下文
                Console.WriteLine("主線程權限測試:");
                JudgePermission(null);
                // 建立一個子線程 subThread2
                Console.WriteLine("子線程權限測試:");
                Thread subThread2 = new Thread(JudgePermission);
                subThread2.Start();
                subThread2.Join();

                Console.ReadKey();
            }
            finally
            {
                DeleteTestFile();
            }
        }
    }
View Code

  該實例的執行結果以下圖所示,能夠看出,經過前面的兩種方式有效地阻止了主線程的執行上下文流動到新建的線程之中,這樣的機制對於性能的提升有必定的幫助。

  

3、多線程編程中的線程同步

3.1 理解同步塊和同步塊索引

  同步塊是.NET中解決對象同步問題的基本機制,該機制爲每一個堆內的對象(即引用類型對象實例)分配一個同步索引,該索引中只保存一個代表數組內索引的整數。具體過程是:.NET在加載時就會新建一個同步塊數組,當某個對象須要被同步時,.NET會爲其分配一個同步塊,而且把該同步塊在同步塊數組中的索引加入該對象的同步塊索引中。下圖展示了這一機制的實現:

  同步塊機制包含如下幾點:

  ① 在.NET被加載時初始化同步塊數組;

  ② 每個被分配在堆上的對象都會包含兩個額外的字段,其中一個存儲類型指針,而另一個就是同步塊索引,初始時被賦值爲-1;

  ③ 當一個線程試圖使用該對象進入同步時,會檢查該對象的同步索引:

    若是同步索引爲負數,則會在同步塊數組中新建一個同步塊,而且將該同步塊的索引值寫入該對象的同步索引中;

    若是同步索引不爲負數,則找到該對象的同步塊並檢查是否有其餘線程在使用該同步塊,若是有則進入等待狀態,若是沒有則申明使用該同步塊;

  ④ 當一個對象退出同步時,該對象的同步索引被修改成-1,而且相應的同步塊數組中的同步塊被視爲再也不使用。

3.2 C#中的lock關鍵字有啥做用?

  lock關鍵字多是咱們在遇到線程同步的需求時最經常使用的方式,但lock只是一個語法糖,爲何這麼說呢,下面慢慢道來。

  (1)lock的等效代碼實際上是Monitor類的Enter和Exit兩個方法

    private object locker = new object();
    public void Work()
    {
          lock (locker)
          {
              // 作一些須要線程同步的工做
          }
     }

  事實上,lock關鍵字時一個方便程序員使用的語法糖,它等效於安全地使用System.Threading.Monitor類型,它直接等效於下面的代碼:

    private object locker = new object();
    public void Work()
    {
        // 避免直接使用私有成員locker(直接使用有可能會致使線程不安全)
        object temp = locker;
        Monitor.Enter(temp);
        try
        {
            // 作一些須要線程同步的工做
        }
        finally
        {
            Monitor.Exit(temp);
        }
    }

  (2)System.Threading.Monitor類型的做用和使用

  Monitor類型的Enter和Exit方法用來實現進入和退出對象的同步,當Enter方法被調用時,對象的同步索引將被檢查,而且.NET將負責一系列的後續工做來保證對象訪問時的線程同步,而Exit方法的調用則保證了當前線程釋放該對象的同步塊。

  下面的代碼示例演示瞭如何使用lock關鍵字來實現線程同步:

    class Program
    {
        static void Main(string[] args)
        {
            // 多線程測試靜態方法的同步
            Console.WriteLine("開始測試靜態方法的同步:");
            for (int i = 0; i < 5; i++)
            {
                Thread thread = new Thread(Lock.StaticIncrement);
                thread.Start();
            }
            // 這裏等待線程執行結束
            Thread.Sleep(5 * 1000);
            Console.WriteLine("-------------------------------");
            // 多線程測試實例方法的同步
            Console.WriteLine("開始測試實例方法的同步:");
            Lock l = new Lock();
            for (int i = 0; i < 6; i++)
            {
                Thread thread = new Thread(l.InstanceIncrement);
                thread.Start();
            }

            Console.ReadKey();
        }
    }

    public class Lock
    {
        // 靜態方法同步鎖
        private static object staticLocker = new object();
        // 實例方法同步鎖
        private object instanceLocker = new object();

        // 成員變量
        private static int staticNumber = 0;
        private int instanceNumber = 0;

        // 測試靜態方法的同步
        public static void StaticIncrement(object state)
        {
            lock (staticLocker)
            {
                Console.WriteLine("當前線程ID:{0}", Thread.CurrentThread.ManagedThreadId.ToString());
                Console.WriteLine("staticNumber的值爲:{0}", staticNumber.ToString());
                // 這裏能夠製造線程並行執行的機會,來檢查同步的功能
                Thread.Sleep(200);
                staticNumber++;
                Console.WriteLine("staticNumber自增後爲:{0}", staticNumber.ToString());
            }
        }

        // 測試實例方法的同步
        public void InstanceIncrement(object state)
        {
            lock (instanceLocker)
            {
                Console.WriteLine("當前線程ID:{0}",Thread.CurrentThread.ManagedThreadId.ToString());
                Console.WriteLine("instanceNumber的值爲:{0}", instanceNumber.ToString());
                // 這裏能夠製造線程並行執行的機會,來檢查同步的功能
                Thread.Sleep(200);
                instanceNumber++;
                Console.WriteLine("instanceNumber自增後爲:{0}", instanceNumber.ToString());
            }
        }
    }
View Code

  下圖是該實例的執行結果:

  

PS:線程同步自己違反了多線程並行運行的原則,因此咱們在使用線程同步時應該儘可能作到將lock加在最小的程序塊上。對於靜態方法的同步,通常採用靜態私有的引用對象成員,而對於實例方法的同步,通常採用私有的引用對象成員。

3.3 能否使用值類型對象來實現線程同步嗎?

  前面已經說到,在.NET中每一個堆內的對象都會有一個同步索引字段,用以指向同步塊的位置。可是,對於值類型來講,它們的對象是分配在堆棧上的,也就是說值類型是沒有同步索引這一字段的,因此直接使用值類型對象沒法實現線程同步

  若是在程序中對於lock關鍵字使用了值類型對象,會直接致使一個編譯錯誤:

3.4 能否使用引用類型對象自身進行同步?

  引用類型的對象是分配在堆上的,必然會包含同步索引,也能夠分配同步塊,因此原則上能夠在對象的方法內對自身進行同步。而事實上,這樣的代碼也確實能有效地保證線程同步。But,這樣的代碼健壯性存在必定問題。

  (1)lock(this)

  回顧lock(this)的設計,就能夠看出問題來:this表明了執行代碼的當前對象,能夠預見該對象能夠被任何使用者訪問,這就致使了不只對象內部的代碼在爭用同步塊,連類型的使用者也能夠有意無心地進入到爭用的隊伍中→這顯然不符合設計意圖

  下面經過一個代碼示例展現了一個惡意的使用者是如何致使類型死鎖的:

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("開始使用");
            SynchroThis st = new SynchroThis();
            // 模擬惡意的使用者
            Monitor.Enter(st);
            // 正常的使用者會收到惡意使用者的影響
            // 下面的代碼徹底正確,但卻被死鎖
            Thread thread = new Thread(st.Work);
            thread.Start();
            thread.Join();
            // 程序不會執行到這裏
            Console.WriteLine("使用結束");

            Console.ReadKey();
        }
    }

    public class SynchroThis
    {
        private int number = 0;

        public void Work(object state)
        {
            lock (this)
            {
                Console.WriteLine("number如今的值爲:{0}", number.ToString());
                number++;
                // 模擬作了其餘工做
                Thread.Sleep(200);
                Console.WriteLine("number自增後值爲:{0}", number.ToString());
            }
        }
    }
View Code

  運行這個示例,咱們發現程序徹底被死鎖,這是由於一個惡意的使用者在使用了同步塊以後卻沒有對其進行釋放,致使了SynchroThis類型的方法被組織。

  (2)lock(typeof(類型名))

  這樣的設計有時候會被用來在靜態方法中實現線程同步,由於靜態方法的訪問須要經過類型來進行,但它也和lock(this)同樣,缺少健壯性。下面展現了常見的錯誤使用代碼示例:

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("開始使用");
            SynchroThis st = new SynchroThis();
            // 模擬惡意的使用者
            Monitor.Enter(typeof(SynchroThis));
            // 正常的使用者會收到惡意使用者的影響
            // 下面的代碼徹底正確,但卻被死鎖
            Thread thread = new Thread(SynchroThis.Work);
            thread.Start();
            thread.Join();
            // 程序不會執行到這裏
            Console.WriteLine("使用結束");

            Console.ReadKey();
        }
    }

    public class SynchroThis
    {
        private static int number = 0;

        public static void Work(object state)
        {
            lock (typeof(SynchroThis))
            {
                Console.WriteLine("number如今的值爲:{0}", number.ToString());
                number++;
                // 模擬作了其餘工做
                Thread.Sleep(200);
                Console.WriteLine("number自增後值爲:{0}", number.ToString());
            }
        }
    }
View Code

  能夠發現,當一個惡意的使用者對type對象進行同步時,也會形成全部的使用者被死鎖。

PS:應該徹底避免使用this對象和當前類型對象做爲同步對象,而應該在類型中定義私有的同步對象,同時應該使用lock而不是Monitor類型,這樣能夠有效地減小同步塊不被釋放的狀況。

3.5 互斥體是個什麼鬼?Mutex和Monitor兩個類型的功能有啥區別?

  (1)什麼是互斥體?

  在操做系統中,互斥體(Mutex)是指某些代碼片斷在任意時間內只容許一個線程進入。例如,正在進行一盤棋,任意時刻只容許一個棋手往棋盤上落子,這和線程同步的概念基本一致。

  (2).NET中的互斥體

  Mutex類是.NET中爲咱們封裝的一個互斥體類型,和Mutex相似的還有Semaphore(信號量)等類型。下面的示例代碼展現了Mutext類型的使用:

    class Program
    {
        const string testFile = "C:\\TestMutex.txt";
        /// <summary>
        /// 這個互斥體保證全部的進程都能獲得同步
        /// </summary>
        static Mutex mutex = new Mutex(false, "TestMutex");

        static void Main(string[] args)
        {
            //留出時間來啓動其餘進程
            Thread.Sleep(3000);
            DoWork();
            mutex.Close();
            Console.ReadKey();
        }

        /// <summary>
        /// 往文件裏寫連續的內容
        /// </summary>
        static void DoWork()
        {
            long d1 = DateTime.Now.Ticks;
            mutex.WaitOne();
            long d2 = DateTime.Now.Ticks;
            Console.WriteLine("通過了{0}個Tick後進程{1}獲得互斥體,進入臨界區代碼。", (d2 - d1).ToString(), Process.GetCurrentProcess().Id.ToString());

            try
            {
                if (!File.Exists(testFile))
                {
                    FileStream fs = File.Create(testFile);
                    fs.Dispose();
                }
                for (int i = 0; i < 5; i++)
                {
                    // 每次都保證文件被關閉再從新打開
                    // 肯定有mutex來同步,而不是IO機制
                    using (FileStream fs = File.Open(testFile, FileMode.Append))
                    {
                        string content = "【進程" + Process.GetCurrentProcess().Id.ToString() +
                            "】:" + i.ToString() + "\r\n";
                        Byte[] data = Encoding.Default.GetBytes(content);
                        fs.Write(data, 0, data.Length);
                    }
                    // 模擬作了其餘工做
                    Thread.Sleep(300);
                }
            }
            finally
            {
                mutex.ReleaseMutex();
            }
        }
    }
View Code

  模擬多個用戶,執行上述代碼,下圖就是在個人計算機上的執行結果:

  如今打開C盤目錄下的TestMutext.txt文件,將看到以下圖所示的結果:

      

  (3)Mutex和Monitor的區別

  這二者雖然都用來進行同步的功能,但實現方法不一樣,其最顯著的兩個差異以下:

  ① Mutex使用的是操做系統的內核對象,而Monitor類型的同步機制則徹底在.NET框架之下實現,這就致使了Mutext類型的效率要比Monitor類型要低不少

  ② Monitor類型只能同步同一應用程序域中的線程,而Mutex類型卻能夠跨越應用程序域和進程

3.6 如何使用信號量Semaphore?

  這裏首先借用阮一峯的《進程與線程的一個簡單解釋》中的介紹來講一下Mutex和Semaphore:

  一個防止他人進入的簡單方法,就是門口加一把鎖。先到的人鎖上門,後到的人看到上鎖,就在門口排隊,等鎖打開再進去。這就叫"互斥鎖"(Mutual exclusion,縮寫 Mutex),防止多個線程同時讀寫某一塊內存區域。

  還有些房間,能夠同時容納n我的,好比廚房。也就是說,若是人數大於n,多出來的人只能在外面等着。這比如某些內存區域,只能供給固定數目的線程使用。

  這時的解決方法,就是在門口掛n把鑰匙。進去的人就取一把鑰匙,出來時再把鑰匙掛回原處。後到的人發現鑰匙架空了,就知道必須在門口排隊等着了。這種作法叫作"信號量"(Semaphore),用來保證多個線程不會互相沖突。

  不難看出,mutex是semaphore的一種特殊狀況(n=1時)。也就是說,徹底能夠用後者替代前者。可是,由於mutex較爲簡單,且效率高,因此在必須保證資源獨佔的狀況下,仍是採用這種設計。

  如今咱們知道了Semaphore是幹啥的了,再把目光放到.NET中的Sempaphore上。Semaphore 繼承自WaitHandle(Mutex也繼承自WaitHandle),它用於鎖機制,與Mutex不一樣的是,它容許指定數量的線程同時訪問資源,在線程超過數量之後,則進行排隊等待,直到以前的線程退出。Semaphore很適合應用於Web服務器這樣的高併發場景,能夠限制對資源訪問的線程數。此外,Sempaphore不須要一個鎖的持有者,一般也將Sempaphore聲明爲靜態的。

  下面的示例代碼演示了4條線程想要同時執行ThreadEntry()方法,但同時只容許2條線程進入:

    class Program
    {
        // 第一個參數指定當前有多少個「空位」(容許多少條線程進入)
        // 第二個參數指定一共有多少個「座位」(最多容許多少個線程同時進入)
        static Semaphore sem = new Semaphore(2, 2);

        const int threadSize = 4;

        static void Main(string[] args)
        {
            for (int i = 0; i < threadSize; i++)
            {
                Thread thread = new Thread(ThreadEntry);
                thread.Start(i + 1);
            }

            Console.ReadKey();
        }

        static void ThreadEntry(object id)
        {
            Console.WriteLine("線程{0}申請進入本方法", id);
            // WaitOne:若是還有「空位」,則佔位,若是沒有空位,則等待;
            sem.WaitOne();
            Console.WriteLine("線程{0}成功進入本方法", id);
            // 模擬線程執行了一些操做
            Thread.Sleep(100);
            Console.WriteLine("線程{0}執行完畢離開了", id);
            // Release:釋放一個「空位」
            sem.Release();
        }
    }
View Code

  上面示例的執行結果以下圖所示:

  

  若是將資源比做「座位」,Semaphore接收的兩個參數中:第一個參數指定當前有多少個「空位」(容許多少條線程進入),第二個參數則指定一共有多少個「座位」(最多容許多少個線程同時進入)。WaitOne()方法則表示若是還有「空位」,則佔位,若是沒有空位,則等待;Release()方法則表示釋放一個「空位」。

感嘆一下:人生中有不少人在你的城堡中進進出出,城中的人想出去,城外的人想衝進來。But,一我的身邊的位置只有那麼多,你能給的也只有那麼多,在這個狹小的圈子裏,有些人要進來,就有一些人不得不離開

參考資料

(1)朱毅,《進入IT企業必讀的200個.NET面試題》

(2)張子陽,《.NET之美:.NET關鍵技術深刻解析》

(3)王濤,《你必須知道的.NET》

(4)阮一峯,《進程與線程的一個簡單解釋

 

相關文章
相關標籤/搜索