乾貨分享:詳解線程的開始和建立

原文地址:C#多線程之旅(2)——建立和開始線程
html

C#多線程之旅目錄:git

C#多線程之旅(1)——介紹和基本概念github

C#多線程之旅(2)——建立和開始線程web

C#多線程之旅(3)——線程池多線程

C#多線程之旅(4)——APM初探app

C#多線程之旅(5)——同步機制介紹函數

C#多線程之旅(6)——詳解多線程中的鎖post

更多文章正在更新中,敬請期待......url

C#多線程之旅(2)——建立和開始線程spa

代碼下載

Thread_博客園_cnblogs_jackson0714.zip

第一篇~第三篇的代碼示例:

源碼地址:https://github.com/Jackson0714/Threads

 

1、線程的建立和開始

在第一篇的介紹中,線程使用Thread 類的構造函數來建立,經過傳給一個ThreadStart 委託來實現線程在哪裏開始執行。下面是ThreadStart的定義:

// Summary:
//     Represents the method that executes on a System.Threading.Thread.
[ComVisible(true)]
public delegate void ThreadStart();

調用一個Start方法,而後設置它開始運行。線程會一直運行直到這個方法返回,而後這個線程結束。

下面是一個例子,使用擴展C#語法建立一個ThreadStart委託:2.1_ThreadStart

 1 class ThreadTest
 2 {
 3     static void Main()
 4     {
 5         Thread t = new Thread(new ThreadStart(Go));
 6         t.Start(); 
 7         Go();
 8         Console.ReadKey();
 9     }
10     static void Go()
11     {
12         Console.WriteLine("hello!");
13     }
14 }

在這個例子中,thread t執行Go(),基本上與主線同時程調用Go()方法,結果是打印出兩個時間接近的hello

一個線程能夠被方便的建立經過指定一個方法組,而後由C#推斷出ThreadStart委託:2.2_Thread

 1 class Program
 2 {
 3     static void Main(string[] args)
 4     {
 5         Thread t = new Thread(Go);
 6         t.Start();
 7         Go();
 8         Console.ReadKey();
 9     }
10 
11     static void Go()
12     {
13         Console.WriteLine("Go");
14     }
15 }

另一種更簡單的方式是使用lambda表達式或者匿名方法:2.3_LambaExpression

static void Main(string[] args)
{
    Thread t = new Thread(()=>Console.WriteLine("Go"));
    t.Start();
    Console.ReadKey();
}

2、傳遞數據給一個線程

1.利用Lambda傳遞一個數據

傳遞參數給線程的目標方法的最簡單的方法是執行一個lambda表達式,該表達式調用一個方法並傳遞指望的參數給這個方法。

2.4_PassingDataToAThread

static void Main(string[] args)
{
    Thread t = new Thread(() => Print("A"));
    t.Start();
    Console.ReadKey();
}

static void Print(string message)
{
    Console.WriteLine(message);
}

2.傳遞多個參數

經過這種方式,你能夠傳遞任意數量的參數給這個方法。你甚至能夠將整個實現包裝在一個多語句的lambda中:

2.5_PassingDataToAThread

new Thread(() =>
{
    Console.WriteLine("a");
    Console.WriteLine("b");
}).Start();

你也能夠簡單的在C# 2.0裏面那樣使用匿名方法作一樣的事:

new Thread(delegate()
{
    Console.WriteLine("a");
    Console.WriteLine("b");
}).Start();

3.利用Thread.Start傳遞參數

另一種方式是傳遞一個參數給ThreadStart方法:

2.6_PassingDataToAThread_ThreadStart

static void Main(string[] args)
{
    Thread t = new Thread(Print);
    t.Start("A");
    Console.ReadKey();
}
static void Print(object messageObj)
{
    string message = (string)messageObj;//必須進行轉換
    Console.WriteLine(message);
}

這種方式可以工做是由於Thread的構造函數是重載的,接受下面兩種中的任意一種委託:

// Summary:
//     Represents the method that executes on a System.Threading.Thread.
[ComVisible(true)]
public delegate void ThreadStart();

// Summary:
//     Represents the method that executes on a System.Threading.Thread.
//
// Parameters:
//   obj:
//     An object that contains data for the thread procedure.
[ComVisible(false)]
public delegate void ParameterizedThreadStart(object obj);

這個ParameterizedThreadStart的只容許接收一個參數。並且由於它的類型是object,因此一般須要轉換。

4.Lambda表達式和捕獲變量

由咱們上面看到的例子能夠知道,一個lambda式在傳遞數據給線程是最用的。然而,你必須很是當心在開始線程後意外修改捕獲變量,由於這些變量是共享的。好比下面的:

2.7_LbdaExpressionsAndCapturedVariables

for(int i =0;i<10;i++)
{
    new Thread(() => Console.Write(i)).Start();
}

這個輸出是不肯定的,下面是一種典型的狀況:

這裏的問題是變量ifor循環執行時指向同一個內存地址。所以,每個線程調用Console.Write時,i的值有可能在這個線程運行時改變。

解決方案是使用一個臨時變量:

2.8_LambdaExpressionsAndCapturedVariables_Solution

for (int i = 0; i < 10; i++)
{
    int temp = i;
    new Thread(() => Console.Write(temp)).Start();
}

變量temp在每一個循環迭代中位於不一樣的內存塊。所以每個線程捕獲到了不一樣的內存位置,並且沒有問題。咱們能夠解釋在以前的代碼中的問題:

2.9_PassingData_TemporaryVariable

string text = "A";
Thread a = new Thread(() => Console.WriteLine(text));

text = "B";
Thread b = new Thread(() => Console.WriteLine(text));

a.Start();
b.Start();

由於兩個lambda表達式捕獲一樣的text的值,因此B被打印出兩次。

3、命名線程

每個線程有一個Name屬性你能夠方便用來debugging.當線程顯示在Visual Statudio裏面的Threads WindowDebug Loaction toolbar的時候,線程的Name屬性是特別有用的。你能夠只設置線程的名字一次;以後嘗試改變它將會拋出異常信息。

靜態的Thread.CurrentThread屬性表明當前執行的線程。

在下面的例子2.10_NamingThread中,咱們設置了主線程的名字:

static void Main(string[] args)
{
    Thread.CurrentThread.Name = "Main Thread";
    Thread t = new Thread(Go);
    t.Name = "Worker Thread";
    t.Start();
    Go();
    Console.ReadKey();
}
static void Go()
{
    Console.WriteLine("Go! The current thread is {0}", Thread.CurrentThread.Name);
}

4、前臺線程和後臺線程

默認狀況下,你本身顯示建立的線程是前臺線程。前臺線程保持這個應用程序一直存活只要其中任意一個正在運行,然後臺線程不是這樣的。一旦全部的前臺線程完成,這個應用程序就結束了, 任何正在運行的後臺線程馬上終止。

一個線程前臺/後臺的狀態跟它的優先級和配置的執行時間沒有關聯。

你可使用線程的IsBackgroud屬性查詢或改變一個線程的後臺狀態。

下面是例子:2.11_PriorityTest

static void Main(string[] args)
{
    Thread t = new Thread(() => Console.ReadKey());
    if (args.Length > 0)//若是Main方法沒有傳入參數
    {
        //設置線程爲後臺線程,等待用戶輸入。
        //由於主線程在t.Start()執行以後就會終止,
        //因此後臺線程t會在主線程退出以後,當即終止,應用程序就會結束。
        t.IsBackground = true;
    }
    t.Start();
}

若是程序調用的時候傳入了參數,則建立的線程爲前臺線程,而後等待用戶輸入。

同時,若是主線程退出,應用程序將不會退出,由於前臺線程t沒有退出。

另外一方面,若是main方法傳入了參數,則建立的線程設置爲後臺線程。當主線程退出時,應用程序當即退出。

當一個進程以這種方式終止,則任何後臺線程執行棧裏面的finally 語句塊將會被規避。

若是你的線程使用finally(or using)語句塊去執行如釋放資源或者刪除臨時文件的清理工做,這將是一個問題。爲了不這個,你能夠顯示地等待後臺線程退出應用程序。

這裏有兩種實現方式:

  1. 若是你本身建立了這個線程,能夠在這個線程上調用Join方法。
  2. 若是你使用線程池,可使用一個事件去等待處理這個線程。

在這兩種狀況下,你須要指定一個timeout,所以能夠結束一個因爲某些緣由拒絕完成的線程。這是你的備選退出策略:在最後,你想要你的應用程序關閉,不須要用戶從任務管理器中刪除。

若是用戶使用任務管理器強制結束一個.NET進程,全部的線程像是後臺線程同樣終止。這個是觀察到的行爲,因此會由於CLR和操做系統的版本而不一樣。

前臺線程不須要這樣對待,可是你必須當心避免可能形成線程不能結束的bugs。形成應用程序不能正確地退出的一個一般的緣由是有激活的前臺線程還存活在。

5、線程優先級

一個線程的優先級決定了在操做系統中它能夠獲得多少相對其餘線程的執行時間,下面是線程優先級的等級:

// Summary:
//     Specifies the scheduling priority of a System.Threading.Thread.
[Serializable]
[ComVisible(true)]
public enum ThreadPriority
{
    Lowest = 0,
    BelowNormal = 1,
    Normal = 2,
    AboveNormal = 3,
    Highest = 4,
}

當多線程同時是激活的,線程優先級是很重要的。

注意:提升線程優先級時,須要很是當心,這將可能致使其餘線程對資源訪問的飢餓狀態的問題。

當提高一個線程的優先級時,不會使它執行實時工做,由於它被應用程序的進程優先級限制了。爲了執行實時工做,你也必須經過使用System.DiagnosticesProcess類來提高進程的優先級:

using (Process p = Process.GetCurrentProcess())
{
    p.PriorityClass = ProcessPriorityClass.High;
}

ProcessPriorityClass.High事實上是優先級最高的一檔:實時。設置一個進程優先級到實時狀態將會致使其餘線程沒法得到CPU時間片。若是你的應用程序意外地進入一個無限循環的狀態,你甚至會發現操做被鎖住了,只有電源鍵可以拯救你了。針對這個緣由,High一般對於實時應用程序是最好的選擇。

若是你的實時應用程序有一個用戶界面,提升程序的優先級將會使刷新界面佔用昂貴的CPU的時間,且會使整個系統變得運行緩慢(尤爲是UI很複雜的時候)。下降主線程優先級且提高進程的優先級來確保實時線程不會被界面重繪所搶佔,可是不會解決其餘進程對CPU訪問缺少的問題,由於操做系統總體上會一直分配不成比例的資源給進程。一個理想的解決方案是讓實時線程和用戶界面用不一樣的優先級運行在不一樣的進程中,經過遠程和內存映射文件來通訊。即便提升了進程優先級,在託管環境中處理硬實時系統需求仍是對適用性有限制。此外,潛藏的問題會被自動垃圾回收引進,操做系統會遇到新的挑戰,即便是非託管代碼,使用專用硬件或者特殊的實時平臺,那將被最好的解決。

6、異常處理

在任何try/catch/finally 語句塊做用域內建立的線程,當這個線程開始時,這個線程和語句塊是沒有關聯的。

思考下面的程序:

 參考例子:2.12_ExceptionHandling

static void Main(string[] args)
{
    try
    {
        new Thread(Go).Start();
    }
    catch(Exception ex)
    {
        Console.WriteLine("Exception");
    }
    Console.ReadKey();
}
static void Go()
{
    throw null;
}

try/catch 聲明在這個例子中是無效的,並且新建立的線程將會被一個未處理的NullReferenceException所阻斷。當你考慮每個線程有一個單獨的執行路徑這種行爲是說得通的。

改進方法是將exception handler移到Go()的方法中:

參考例子:2.13_ExceptionHandling_Remedy

class Program
{
    static void Main(string[] args)
    {
        new Thread(Go).Start();
        Console.ReadKey();
    }

    static void Go()
    {
        try
        {
            throw null;
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
}

你須要在應用程序中的全部線程入口方法中添加一個exception handler ,就像你在主線程中作的那樣。一個未處理的線程會形成整個應用程序關閉,並且會彈出一個很差看的窗口。

在寫這個exception handling 語句塊時,你可能極少忽略這個問題,典型狀況是,你可能會記錄exception的詳細信息,而後可能顯示一個窗口讓用戶去自動去提交這些信息到你的web server上。而後你可能會關掉這個應用程序-由於這個error毀壞了程序的狀態。而後,這樣作的開銷是用戶可能會丟失他最近的工做,好比打開的文檔。

對於WPFWinForm應用程序來講,全局的exception handling 事件(Application.DispatcherUnhandlerException Application.ThreadException)只會檢測到主UI線程上的拋出的異常。你仍是必須手動處理線程的異常。

AppDomain.CurrentDomain.UnhandledException能夠檢測任何未處理的異常,可是沒法阻止應用程序以後關閉。

然而,某些情形下你不須要在線程上處理異常,由於.NET Framework爲你作了這個。下面是沒有說起的內容:

Asynchronous delegates

BackgroudWorker

The Task Parallel Library(conditions apply)

相關文章
相關標籤/搜索