天天看點

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

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

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

C#多線程之旅目錄:

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

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

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

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

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

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

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

代碼下載下傳

Thread_部落格園_cnblogs_jackson0714.zip

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

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

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

一、線程的建立和開始

在第一篇的介紹中,線程使用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();
}      

二、傳遞資料給一個線程

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傳遞參數

另外一種方式是傳遞一個參數給Thread的Start方法:

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();
}      

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

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

這裡的問題是變量i在for循環執行時指向同一個記憶體位址。是以,每一個線程調用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被列印出兩次。

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

三、命名線程

每一個線程有一個Name屬性你可以友善用來debugging.當線程顯示在Visual Statudio裡面的Threads Window和Debug 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);
}      

四、前台線程和背景線程

預設情況下,你自己顯示建立的線程是前台線程。前台線程保持這個應用程式一直存活隻要其中任意一個正在運作,而背景線程不是這樣的。一旦所有的前台線程完成,這個應用程式就結束了, 任何正在運作的背景線程立刻終止。

一個線程前台/背景的狀态跟它的優先級和配置的執行時間沒有關聯。

你可以使用線程的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。造成應用程式不能正确地退出的一個通常的原因是有激活的前台線程還存活在。

五、線程優先級

一個線程的優先級決定了在作業系統中它可以得到多少相對其他線程的執行時間,下面是線程優先級的等級:

// 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.Diagnostices的Process類來提升程序的優先級:

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

ProcessPriorityClass.High事實上是優先級最高的一檔:實時。設定一個程序優先級到實時狀态将會導緻其他線程無法獲得CPU時間片。如果你的應用程式意外地進入一個無限循環的狀态,你甚至會發現操作被鎖住了,隻有電源鍵能夠拯救你了。針對這個原因,High通常對于實時應用程式是最好的選擇。

如果你的實時應用程式有一個使用者界面,提高程式的優先級将會使重新整理界面占用昂貴的CPU的時間,且會使整個系統變得運作緩慢(尤其是UI很複雜的時候)。降低主線程優先級且提升程序的優先級來確定實時線程不會被界面重繪所搶占,但是不會解決其他程序對CPU通路缺乏的問題,因為作業系統整體上會一直配置設定不成比例的資源給程序。一個理想的解決方案是讓實時線程和使用者界面用不同的優先級運作在不同的程序中,通過遠端和記憶體映射檔案來通信。即使提高了程序優先級,在托管環境中處理硬實時系統需求還是對适用性有限制。此外,潛藏的問題會被自動垃圾回收引進,作業系統會遇到新的挑戰,即使是非托管代碼,使用專用硬體或者特殊的實時平台,那将被最好的解決。

六、異常處理

在任何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毀壞了程式的狀态。然後,這樣做的開銷是使用者可能會丢失他最近的工作,比如打開的文檔。

對于WPF和WinForm應用程式來說,全局的exception handling 事件(Application.DispatcherUnhandlerException 和Application.ThreadException)隻會檢測到主UI線程上的抛出的異常。你還是必須手動處理線程的異常。

AppDomain.CurrentDomain.UnhandledException可以檢測任何未處理的異常,但是無法阻止應用程式之後關閉。

然而,某些情形下你不需要線上程上處理異常,因為.NET Framework為你做了這個。下面是沒有提及的内容:

Asynchronous delegates

BackgroudWorker

The Task Parallel Library(conditions apply)

繼續閱讀