多線程使用~會多少?

前言

多線程就是容許複雜的應用程序在同一時刻執行多項任務,.NET FrameWork的託管編碼環境提供了一個完整而強大的線程模型,該模型容許編程人員精確控制在一個線程中的內容,線程什麼時候退出,以及它訪問多少數據。git

本文將要介紹何時用到線程、如何使用、遇到的坑。編程

何時使用線程

   實際上,全部的程序都是在線程中執行的,因此理解.NET 和 Windows 如何執行線程,將有助於理解程序在運行時期的執行狀況,Windows Form應用程序使用事件循環線程來處理用戶界面交互,各個窗體在不一樣線程上執行,若是須要在Windows Forms之間交互,就須要在線程之間交互,ASP.NET 頁面在IIS 的 多線程環境中執行一一同一頁面上不一樣請求能夠在不一樣線程上執行,同一頁面能夠同時在多個線程中執行。在訪問ASP.NET 頁面上的共享資源時,就會出現遇到線程問題。api

線程

  線程技術是指開發架構將應用程序一部分分爲」線程「,使線程與程序其他部分的執行順序不一樣,在絕大編程語言中,都有至關於Main()的方法,該方法中的每一行都要按順序執行,下一行代碼要在上一行代碼執行完成以後再執行,線程是一種特殊的情況,是操做系統執行多任務的一部分,它容許應用程序的一部分獨立於其餘對象而單獨執行,所以就脫離了正常的執行順序安全

進程

  當啓動應用程序的時候,系統就會自動爲系統分配內存和其餘相關資源,內存和資源的物理分離就叫作進程,固然,應用程序能夠啓動多個進程,要  記住的是"應用程序"與"進程"並非同義詞。分配給進程的內存爲其餘進程分配的內存被隔離,只有所屬的那個進程才能夠訪問它,因此每一個進程都應有一個線程!多線程

  在Windows中,經過訪問Windows任務管理器,就能夠很是直觀的查看當前運行的進程。右擊任務欄的空白選項就能夠看到了。架構

 NET和C#對線程的支持

  .NET支持自由線程,全部的屬於.NET的語言均可以使用線程,包括C#和VB.NET以及F#,前面說過"進程"是一個內存和資源的一個物理獨立的部分。後來又提到,一個進程至少有一個線程。當MicroSoft設計.NET Framework時,新增了一個隔離層,稱爲應用程序域或AppDomain,應用程序並不向進程那樣獨立存在,而是進程內部的一個邏輯獨立的部分。在一個進程中能夠有多個程序域,這是很是不錯的優勢,一般,標準的進程不是用代理,就不能夠訪問其餘進程的數據。使用代理很是消耗系統開銷,編碼也變得複雜,但是如咱們引入程序域的概念,就能夠在一個進程中啓動多個應用程序。進程所提供的隔離區能夠和應用程序一塊兒使用,線程能夠跨多個應用程序來執行,還不須要消耗因線程內部的分配而帶來的系統開銷,還有一個好處是,它們提供了類型檢查功能。app

   Microsoft把這些應用程序域的全部功能都封裝在了一個名爲System,AppDomain的類中,Microsoft.NET程序集與這些應用程序域有着很是密切的關係,只要將程序集加載到應用程序中,它就會載入AppDomain。除非特別指出,不然程序集就會被加載到調用代碼的AppDomain中。應用程序域與線程也有直接的關係,它們能夠保存一個或多個線程,就像進程同樣。但是不一樣之處是,應用程序域能夠在進程內建立,但不是建立新線程。  dom

定義線程

瞭解了有關的理論和模型後,如今要介紹一些實際的代碼,下面的實例將使用AppDomain來設置數據,檢索數據以及表示AppDomain在執行的線程,建立一個appdomain.cs。編程語言

 

//定義應用程序域
        public AppDomain Domain;
        public int ThreadId; //設置值 public void SetDomainData(string vName,string vValue) { Domain.SetData(vName,(object)vValue); ThreadId = AppDomain.GetCurrentThreadId(); } //獲取值 public string GetDomainData(string name) { return (string)Domain.GetData(name); } public static void DomainMainGo() { string DataName = "MyData"; string DataValue = "Some Data to be stored"; Console.WriteLine("Retrieving current domain"); MyAppDomain Obj = new MyAppDomain(); Obj.Domain = AppDomain.CurrentDomain; Console.WriteLine("Setting domain data"); Obj.SetDomainData(DataName,DataValue); Console.WriteLine("Getting domain data"); Console.WriteLine($"The Data found for key{DataName},is{Obj.GetDomainData(DataName)},running on thread id: {Obj.ThreadId}"); }

執行程序後,獲得以下結果。函數

這對於沒有什麼經驗的C#開發人員說也很簡單,下面看一下代碼,說明發生了什麼。這個類中第一段重要的代碼以下所示:

public void SetDomainData(string vName,string vValue)
        {
            Domain.SetData(vName,(object)vValue); ThreadId = AppDomain.GetCurrentThreadId(); }
      這個方法的參數是要設置的數據名稱及其值。注意當SetData()方 法傳入參數時,作了一些不同凡響的工做。這裏將String 類型的值強制轉換爲object 數據類型,由於SetData()方法把object 數據類型做爲其第二個參數。該方法只使用了一個String, 而String由System.Object派生而來,因此不用將這個變量強制轉換爲object數據類型。不過,要存儲其餘數據,就沒這麼容易處理了,進行這個轉換隻是提醒一下。在這個方法的最後,調用AppDomain對象的GetCurrentThreadId屬性,就能夠獲得當前運行的ThreadId.
  再說一下Get的這個方法。
public string GetDomainData(string name)
        {
            return (string)Domain.GetData(name); }

 這個方法很是簡單,這裏使用AppDomain類的GetData()方法,根據鍵值獲取數據。把參數從GetDomainData方法傳遞給GetData,再把該方法的結果傳遞迴調用方法。

NET中的線程

剛纔說了線程是什麼,介紹了不少基本知識,還有一些重要的概念,那麼如今說一說.NET中的線程。說到這裏你必定想起來了Systeam.Threading,這裏就不列出那些屬性或者方法表了,太多了。

這裏介紹一個簡單的栗子,它並不適合於解釋爲何要用線程,但去除了稍後講到的全部複雜因素,建立一個新的控制檯應用程序,把文件命名爲simple_thread.cs。

public class simple_thread
    {
        public void SimpleMethod() { int i = 5; int x = 10; int result = i * x; Console.WriteLine($"This code calculated the value {result.ToString()} from threadID:{AppDomain.GetCurrentThreadId().ToString()}"); } public static void MainGo() { simple_thread simple = new simple_thread(); simple.SimpleMethod(); ThreadStart ts = new ThreadStart(simple.SimpleMethod); Thread t = new Thread(ts); t.Start(); } }

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

如今簡單解釋一下這個簡單的代碼,線程的功能都是封裝在System.Threading命名空間中,所以,必須將這個命名空間導入到項目中。一旦導入了這個命名空間,就能夠創建一個能在主線程上和新工做線程上執行的方法。在第二次執行SimpleMethod方法代碼的時候,其實就是在另外一個線程上執行的。

上面的示例中,咱們說明不了什麼,由於咱們沒法顯示不一樣的線程ID,畢竟尚未執行多個線程。爲了模擬更高的真實性,咱們再建立一個類,名爲:do_something_thread.cs,其定義以下:

    public class DoSomethingThread
    {
        static void WorkerMethod() { for (int i=1;i<1000;i++) { Console.WriteLine("Worker Thread:"+i.ToString()); } } static void MainGo() { ThreadStart ts = new ThreadStart(WorkerMethod); Thread t = new Thread(ts); t.Start(); for (int i=0;i<1000;i++) { Console.WriteLine("Primary Thread"+i.ToString()); } } }

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

      由於這些代碼沒有涉及新的編碼技術,因此這裏再也不逐一介紹。不過能夠看出,執行時間在兩個線程之間共享。在一個線程完成以前,另外一個線程並無徹底中止。每一個線程都會有一小段時間米執行。在一個線程用完它的執行時間後,下一個線程就開始在它的時間片中執行。這兩個線程會一直這樣交替執行下去,直到執行完畢。實際上,系統上有多於兩個的線程在交替執行,共享時間片。在前面的應用程序中,就不僅是在兩個線程之間來回切換。事實上,該應用程序的線程在與當前運行在計算機上.的許多線程起 共享執行時間。
  如今回頭看一下ThreadStart委託,使用這些委託k能夠完成一些有趣的事情。好比呢,咱們都作事後天權限管理,咱們將要實現一個不一樣的角色去登陸後臺那麼就會有不一樣的場景,列入在管理員登陸後,就要運行一個後臺進程,來收集報告數據。當報告完成後,ji就通知管理員,而對於普通用戶,就不須要這個功能了,這就是ThreadStart的面向對象特性。下面咱們建立一個ThreadStartBranching.cs。
    public class ThreadStartBranching
    {
        enum UserClass { ClassAdmin, ClassUser } static void AdminMethod() { Console.WriteLine("Admin Method"); } static void UserMethod() { Console.WriteLine("User Method"); } static void ExecuteFor(UserClass uc) { ThreadStart ts; ThreadStart tsAdmin = new ThreadStart(AdminMethod); ThreadStart tsUser = new ThreadStart(UserMethod); if (uc == UserClass.ClassAdmin) ts = tsAdmin; else ts = tsUser; Thread t = new Thread(ts); t.Start(); } public static void MainGo() { //excute in the context of an admin user  ExecuteFor(UserClass.ClassAdmin); ExecuteFor(UserClass.ClassUser); Console.ReadLine(); } }

以上代碼的結果是很是簡單的。

 線程的屬性和方法

  Thread類有不少方法和屬性,使用Systeam.Threading 命名空間可使控制線程的執行簡單得多。到目前爲止,咱們只是建立線程並啓動它。

  下面再介紹兩個Thread類的成員:Sleep()方法和IsAlive屬性,線程何以睡眠一段時間,直到時間到了纔會終端睡眠。要使線程睡眠直接Sleep方法便可。觀察下面代碼,建立一個threadSleep.cs文件。

static void WorkFunction()
        {
            string ThreadState; for (int i = 1;i<50000;i++) { if (i % 5000 == 0) { ThreadState = Thread.CurrentThread.ToString(); Console.WriteLine("Worker"+ThreadState); } } Console.WriteLine("Worker Function Complete"); } public static void MainGo() { string ThreadState; Thread t = new Thread(new ThreadStart(WorkFunction)); t.Start(); while (t.IsAlive) { Console.WriteLine("Still waiting.Iam going back to sleep!!"); Thread.Sleep(1); } ThreadState = t.ThreadState.ToString(); Console.WriteLine("He's finally dene! Thread state is "+ ThreadState); }

輸出結果以下:

注意:在for循環中試驗不一樣的值並傳遞到sleep方法中,就能夠看到不一樣的結果。

 首先,建立了一個線程,直接建立了一個ThreadStart變量看成參數。使用IsAlice屬性能夠判斷線程是否還在執行,代碼的其餘部分是沒有標準的,可是要注意其餘的點,首先利用sleep制定線程睡眠,給其餘線程讓出執行時間,傳入的參數單位是毫秒。

 線程的優先級

      線程的優先級決定了各個線程之間相對的優先級。ThreadPriority 枚舉定義了可用f設置線程優先級的值,可用的值是:
      ●Highest(最高值)
      ●AboveNormal(高於正常值)

      ●Normal(正常值)

      ●BelowNormal(低於正常值)

      ●Lowest(最低值)

      當運行庫建立-個線程,但尚未分配優先級時,線程的初始優先級爲Normal。不過,叮以使用ThreadPriority枚舉改變優先級。在介紹線程優先級的例子以前,先討論一下線程的優先級。建立一個簡單的線程實例,顯示當前線程thread_ priority.cs 的名稱、狀態和優先級信息:

 public class ThreadPriority
    {
        public static Thread worker; public static void MainGo() { Console.WriteLine("Entering void Main()"); worker = new Thread(new ThreadStart(FindPriority)); worker.Name = "FindPriority() Thread"; worker.Start(); Console.WriteLine("Exiting void Main()"); } public static void FindPriority() { Console.WriteLine("Name:"+worker.Name); Console.WriteLine("State:"+worker.ThreadState.ToString()); Console.WriteLine("Priority:"+worker.Priority.ToString()); } }

這段代碼很是簡單,定義了方法FindPriority(),該方法顯示了當前線程的名稱、狀態和優先級狀態。工做線程是以Normal優先級運行的。如下是改變優先級的代碼。

worker2.Priority = System.Threading.ThreadPriority.Highest;

 須要注意的是應用程序沒法限制操做系統修改由開發人員爲制定線程分配的優先級,由於應用程序控制着全部線程。它們知道如何給你安排,老鐵。

計時器和回調

  因爲線程不像應用程序的其他代碼那樣次序運行,因此咱們沒法肯定線程影響特定共享資源的動做,是否會在另外一線程訪問共享資源以前完成。因此爲了解決這些問題。使用計時器,能夠按特定的時間執行方法。檢查是否完成。這是一個很是簡單的模型。但能夠利用到不少狀況。

      計時器由兩個對象組成: TimerCallback 和Timer。TimerCallback 委託定義了以指定間隔執行的方法,而Timer對象自己就是計時器。TimerCallback 將- 一個特定的方法與計時器聯繫起來。Timer的構造函數(由重載獲得)須要4個參數。第一個是前面指定的TimerCallback對象,第二個是可用於將狀態傳輸給指定方法的-一個對象。後兩個參數分別是開始調用方法以後的時間,以及之後調用TimerCallback方法的時間間隔。這些參數能夠是整型或者長整型,表示毫秒數。而在下面的內容中,使用的是System.TimeSpan對象,該對象能夠以時鐘滴答、毫秒、秒、分、小時或天爲單位指定間隔。

 public class TimerExample
    {
        private string message;//消息
        private static Timer tmr;//定時器
        private static bool complete;//是否完成
    }

上面的定義很簡單,將tmr聲明爲靜態變量,並適用於整個類。

    public class TimerExample
    {
        private string message;//消息
        private static Timer tmr;//定時器
        private static bool complete;//是否完成
        public void GenerateText() { StringBuilder sb = new StringBuilder(); for (int i=1;i<200;i++) { sb.Append("This is Line"+i.ToString()+ System.Environment.NewLine); } message = sb.ToString(); } public void GetText(object state) { if (message == null) return; Console.WriteLine("message is:"+message); tmr.Dispose(); complete = true; } public void MainGo() { TimerExample obj = new TimerExample(); Thread t = new Thread(new ThreadStart(GenerateText)); t.Start(); TimerCallback timerCallback = new TimerCallback(GetText); tmr = new Timer(timerCallback,null,TimeSpan.Zero,TimeSpan.FromSeconds(2)); do { if (complete) break; } while (true); Console.WriteLine("Exiting Main."); Console.ReadLine(); } }

經過Timer定時器兩秒一次觸發,若是message尚未設置值,那這個方法就會退出,不然就輸出了一個消息。而後由GC刪除了計時器。

 線程的生命週期

  當你安排一個線程的時候,這個線程會經歷幾個狀態,包括未啓動,激活,睡眠狀態,Thread類包含一些方法,容許啓動,中止,恢復,終止,掛起以及等待線程。使用線程的ThreadState能夠肯定線程當前的狀態。這個狀態是一個枚舉值,其定義以下。

    [Flags]
    public enum ThreadState { Running = 0, StopRequested = 1, SuspendRequested = 2, Background = 4, Unstarted = 8, Stopped = 16, WaitSleepJoin = 32, Suspended = 64, AbortRequested = 128, Aborted = 256 }

線程的方法

  線程還四大操做,如線程睡眠,中斷線程,暫停及恢復線程,銷燬線程,鏈接線程。那Sleep()就很少了,當線程進入了睡眠時,他就進入了WaitSleepJoin 狀態。若是線程處於睡眠狀態,在到達指定的睡眠時間以前喚醒線程的方法,就只有Interrupt()了。這個方法會從新放到調度隊列裏;下面建立一個ThreadInterupt.cs.如下是代碼:

喚醒線程

    public class ThreadSleppJoin
    {
        public static Thread sleeper; public static Thread worker; public static void MainGo() { Console.WriteLine("Entering the void Main!"); sleeper = new Thread(new ThreadStart(SleepingThread)); worker = new Thread(new ThreadStart(AwakeTheThread)); sleeper.Start(); worker.Start(); } public static void SleepingThread() { for (int i = 1; i < 50; i++) { Console.Write(i + " "); if (i == 10 || i == 20 || i == 30) { Console.WriteLine("Going to sleep at:" + i); Thread.Sleep(20); } } } public static void AwakeTheThread() { for (int i = 50; i < 100; i++) { Console.Write(i + " "); if (sleeper.ThreadState == System.Threading.ThreadState.WaitSleepJoin) { Console.WriteLine("Interrupting the slepping thread"); sleeper.Interrupt(); } } } }

執行以上代碼結果以下


      在上面的例子中,當計數器達到十、20 和30時,第一個線程(sleeper線程)進入睡眠狀態。第二個線程(worker線程)檢查第一個線程是否處於睡跟狀態。若是是,就中斷第一個線程,將其放回到調度隊列中。Interrupt(方 法是將處於睡眠狀態的線程從新激活的最好方法,若是等待資源的過程結束,且但願線程進入激活狀態,就可使用這項功能。

暫停及恢復線程

   Thread類的Supend()和Resume()方法能夠用來暫停和恢復線程。Supend()方法將無限期地關閉線程,直到另外一個線程將其喚醒。當調用Resume()方法時,線程會處於SuspendRequested或Suspended狀態。

    public partial class Form1 : Form
    {
        private Thread primeNumberThread; public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { primeNumberThread = new Thread(new ThreadStart(AddItemToListBox)); primeNumberThread.Name = "Prime Numbers Example"; primeNumberThread.Priority = ThreadPriority.BelowNormal; primeNumberThread.Start(); } long num = 0; public void AddItemToListBox() { while (1==1) { if (primeNumberThread.ThreadState != System.Threading.ThreadState.Suspended || primeNumberThread.ThreadState != System.Threading.ThreadState.SuspendRequested) { Thread.Sleep(1000); num += 1; this.listBox1.Items.Add(num.ToString()); } else break; } } private void button2_Click(object sender, EventArgs e) { if (primeNumberThread.ThreadState==System.Threading.ThreadState.Running) { primeNumberThread.Suspend(); } } private void Form1_Load(object sender, EventArgs e) { this.Dispose(); } private void button3_Click(object sender, EventArgs e) { if (primeNumberThread.ThreadState != System.Threading.ThreadState.Suspended || primeNumberThread.ThreadState != System.Threading.ThreadState.SuspendRequested) { primeNumberThread.Resume(); } } }

 微軟提示:不要使用SuspendResume方法來同步線程活動。 有沒有辦法知道當你暫停執行一個線程的哪些代碼。 若是您掛起線程安全權限評估期間持有鎖,其餘線程中AppDomain可能被阻止。 若是您掛起線程執行的類構造函數時,其餘線程中AppDomain中嘗試使用類被阻止。 能夠很容易發生死鎖。

 銷燬線程

      Abort()方法能夠用來銷燬當前的線程。若是因某種緣由(好比線程執行了太長時間或用戶選擇了取消)要終止線程,就可使用Abort0方法。例如,若是搜索進程執行了很長時間,就能夠終止該進程。搜索能夠繼續執行,但用戶已經獲得了須要的結果,就再也不須要繼續執行搜索例程上的線程了。在線程上調用Abort()方法時,會引起ThreadAbortException異常。若是沒有在線程的代碼中捕獲該異常,線程就會終止。在多線程環境下訪問的方法中編寫通常異常處理代碼以前,應慎重考慮一下。由於catch(Exception e)也會捕獲ThreadAbortException異常(可能不但願從中恢復)。因而可知,ThreadAbortException異常並不容易中止,程序流也不會像指望的那樣繼續執行。
private void button4_Click(object sender, EventArgs e)
        {
            primeNumberThread.Abort();
        }

這樣一個線程就被銷燬了。就不再存在了,若是還要去使用,就須要再去建立一個實例對象。

鏈接線程

      Join()方法會暫停給定的線程,直到當前的線程終止。在給定的線程實例上調用Join()方法時,線程將置於WaitsleepJoin狀態。若是.一個線程依賴於另.一個線程,就可使用這個方法。鏈接兩個線程的意思是當調用Join(方法時, 運行着的線程將進入WaitsleepJoin狀態,而直到調用Join(方法 的線程完成了任務,該線程纔會返回到Running狀態。這聽起來有點混亂,下面用一個例子thread joining.cs 來講明,其代碼以下所示:
    public class Threadjoining
    {
        public static Thread SecondThread; public static Thread FirstThread; static void First() { for (int i=1;i<=50;i++) { Console.Write(i+" "); } } static void Second() { FirstThread.Join(); for(int i=51;i<=100;i++) { Console.Write(i+" "); } } public static void MainGo() { FirstThread = new Thread(new ThreadStart(First)); SecondThread = new Thread(new ThreadStart(Second)); FirstThread.Start(); SecondThread.Start(); } }

運行以上代碼,結果以下.

      這個簡單的示例的目的就是順序地將數字輸出到控制檯,從1開始到50爲止。First0方法將輸出前50個數字,Second方法則輸出從51到100的數字。若是Second()方法中沒有FirstThread.Join 執行流就會在兩個方法之間求回切換回,輸出結果就會很混亂(試着註釋掉該行,再次運行這個例子)。經過在Second()方 法中調用FirstThread.Join()方法,將暫停Second0方法的執行,直到FirstThread(First0方法)中的代碼執行完畢。
      Join()方法是重載的;它唯-的參數能夠是一個整數,也能夠是一個TimeSpan,該方法將返回一個布爾值。調用這個方法的-一個重載版本後,線程會暫停,直到另外一個線程結束或者超時(以先發生的爲準)爲止。若是線程已經結束,返回值就是True,不然將爲False。

線程不是萬能的

      多線程應用程序須要不少資源。線程須要內存來存儲線程本地的存儲器,所使用的線程數受到可用的內存總數限制。目前,內存是至關便宜的,所以不少計算機有很大的內存。可是,並非全部的計算機都是這樣。若是在未知的硬件配置上運行應用程序,就不能假定應用程序有足夠的內存,也不能假定只有一個進稈會產生線程,消耗系統資源。一臺計算機有不少內存空間,並不意味着全部的內存都由一個應用程序使用。

      每個線程還會致使額外的處理器開銷。若是在應用程序中建立太多的線程,就會限制線程的總執行時間。所以,與執行線程包含的指令相比,處理器在線程之間切換所花的時間會更多。若是應用程序建立了更多的線程,應用程序得到的執行時間將比其餘包含較少線程的進程更多。

 最後

本文的全部相關示例,都是由我測試經過的,有問題的話,在下方留言咱們一塊兒討論。我把代碼放到了Coding上,其地址爲:https://coding.net/u/zaranet/p/ThreadDemo/git/tree/master。  完結!

相關文章
相關標籤/搜索