[轉貼] .Net線程(多執行緒)問題解答

2012042410:09
出處:http://www.cnblogs.com/yizhu2000/archive/2008/01/03/1011958.html

把遇到過的對.Net線程的一些問題和誤解集中起來和大家分享,也希望大家能一起補充,熱烈歡迎討論

目錄

 

基礎篇

WinForm多線程編程篇

線程池

同步

什麼時候需要鎖定

Web和IIS

基礎篇

 

 怎樣創建一個線程

我只簡單列舉幾種常用的方法,詳細可參考.Net多線程總結(一)

一)使用Thread類

ThreadStart threadStart=new ThreadStart(Calculate);//通過ThreadStart委託告訴子線程講執行什麼方法,這裡執行一個計算圓周長的方法
Thread thread=new Thread(threadStart);
thread.Start(); 
//啟動新線程

public void Calculate(){
double Diameter=0.5;
Console.Write(
"The perimeter Of Circle with a Diameter of {0} is {1}"Diameter,Diameter*Math.PI);
}

 

二)使用Delegate.BeginInvoke

delegate double CalculateMethod(double Diameter); //申明一個委託,表明需要在子線程上執行的方法的函數簽名
static CalculateMethod calcMethod = new CalculateMethod(Calculate);//把委託和具體的方法關聯起來
static void Main(string[] args)
{
//此處開始異步執行,並且可以給出一個回調函數(如果不需要執行什麼後續操作也可以不使用回調)
calcMethod.BeginInvoke(5new AsyncCallback(TaskFinished), null);
Console.ReadLine();
}

//線程調用的函數,給出直徑作為參數,計算周長
public static double Calculate(double Diameter)
{
    
return Diameter * Math.PI;
}

//線程完成之後回調的函數
public static void TaskFinished(IAsyncResult result)
{
    
double re = 0;
    re 
= calcMethod.EndInvoke(result);
    Console.WriteLine(re);
}

三)使用ThreadPool.QueueworkItem

WaitCallback w = new WaitCallback(Calculate);
//下面啟動四個線程,計算四個直徑下的圓周長
ThreadPool.QueueUserWorkItem(w, 1.0);
ThreadPool.QueueUserWorkItem(w, 
2.0);
ThreadPool.QueueUserWorkItem(w, 
3.0);
ThreadPool.QueueUserWorkItem(w, 
4.0);
public static void Calculate(double Diameter)
{
return Diameter * Math.PI;
}


下面兩條來自於http://www.cnblogs.com/tonyman/archive/2007/09/13/891912.html

  受托管的線程與 Windows線程

必須要瞭解,執行.NET應用的線程實際上仍然是Windows線程。但是,當某個線程被CLR所知時,我們將它稱為受托管的線程。具體來說,由受托管的代碼創建出來的線程就是受托管的線程。如果一個線程由非托管的代碼所創建,那麼它就是非托管的線程。不過,一旦該線程執行了受托管的代碼它就變成了受托管的線程。

一個受托管的線程和非托管的線程的區別在於,CLR將創建一個System.Threading.Thread類的實例來代表並操作前者。在內部實現中,CLR將一個包含了所有受托管線程的列表保存在一個叫做ThreadStore地方。

CLR確保每一個受托管的線程在任意時刻都在一個AppDomain中執行,但是這並不代表一個線程將永遠處在一個AppDomain中,它可以隨著時間的推移轉到其他的AppDomain中。

從安全的角度來看,一個受托管的線程的主用戶與底層的非托管線程中的Windows主用戶是無關的。
 

  前台線程與後台線程

啟動了多個線程的程序在關閉的時候卻出現了問題,如果程序退出的時候不關閉線程,那麼線程就會一直的存在,但是大多啟動的線程都是局部變量,不能一一的關閉,如果調用Thread.CurrentThread.Abort()方法關閉主線程的話,就會出現ThreadAbortException 異常,因此這樣不行。
後來找到了這個辦法: Thread.IsBackground 設置線程為後台線程。
 
msdn對前台線程和後台線程的解釋:托管線程或者是後台線程,或者是前台線程。後台線程不會使托管執行環境處於活動狀態,除此之外,後台線程與前台線程是一樣的。一旦所有前台線程在托管進程(其中 .exe 文件是托管程序集)中被停止,系統將停止所有後台線程並關閉。通過設置 Thread.IsBackground 屬性,可以將一個線程指定為後台線程或前台線程。例如,通過將 Thread.IsBackground 設置為 true,就可以將線程指定為後台線程。同樣,通過將 IsBackground 設置為 false,就可以將線程指定為前台線程。從非托管代碼進入托管執行環境的所有線程都被標記為後台線程。通過創建並啟動新的 Thread 對像而生成的所有線程都是前台線程。如果要創建希望用來偵聽某些活動(如套接字連接)的前台線程,則應將 Thread.IsBackground 設置為 true,以便進程可以終止。
所以解決辦法就是在主線程初始化的時候,設置:Thread.CurrentThread.IsBackground = true;

這樣,主線程就是後台線程,在關閉主程序的時候就會關閉主線程,從而關閉所有線程。但是這樣的話,就會強制關閉所有正在執行的線程,所以在關閉的時候要對線程工作的結果保存。


經常看到名為BeginXXX和EndXXX的方法,他們是做什麼用的

這是.net的一個異步方法名稱規範
.Net在設計的時候為異步編程設計了一個異步編程模型(APM),這個模型不僅是使用.NET的開發人員使用,.Net內部也頻繁用到,比如所有的Stream就有BeginRead,EndRead,Socket,WebRequet,SqlCommand都運用到了這個模式,一般來講,調用BegionXXX的時候,一般會啟動一個異步過程去執行一個操作,EndEnvoke可以接收這個異步操作的返回,當然如果異步操作在EndEnvoke調用的時候還沒有執行完成,EndInvoke會一直等待異步操作完成或者超時

.Net的異步編程模型(APM)一般包含BeginXXX,EndXXX,IAsyncResult這三個元素,BeginXXX方法都要返回一個IAsyncResult,而EndXXX都需要接收一個IAsyncResult作為參數,他們的函數簽名模式如下

IAsyncResult BeginXXX(...);

<返回類型> EndXXX(IAsyncResult ar);

BeginXXX和EndXXX中的XXX,一般都對應一個同步的方法,比如FileStream的Read方法是一個同步方法,相應的BeginRead(),EndRead()就是他的異步版本,HttpRequest有GetResponse來同步接收一個響應,也提供了BeginGetResponse和EndGetResponse這個異步版本,而IAsynResult是二者聯繫的紐帶,只有把BeginXXX所返回的IAsyncResult傳給對應的EndXXX,EndXXX才知道需要去接收哪個BeginXXX發起的異步操作的返回值。

這個模式在實際使用時稍顯繁瑣,雖然原則上我們可以隨時調用EndInvoke來獲得返回值,並且可以同步多個線程,但是大多數情況下當我們不需要同步很多線程的時候使用回調是更好的選擇,在這種情況下三個元素中的IAsynResult就顯得多餘,我們一不需要用其中的線程完結標誌來判斷線程是否成功完成(回調的時候線程應該已經完成了),二不需要他來傳遞數據,因為數據可以寫在任何變量裡,並且回調時應該已經填充,所以可以看到微軟在新的.Net Framework中已經加強了對回調事件的支持,這總模型下,典型的回調程序應該這樣寫

a.DoWork+=new SomeEventHandler(Caculate);
a.CallBack+=new SomeEventHandler(callback);
a.Run();

(註:我上面講的是普遍的用法,然而BeginXXX,EndXXX僅僅是一種模式,而對這個模式的實現完全取決於使用他的開發人員,具體實現的時候你可以使用另外一個線程來實現異步,也可能使用硬件的支持來實現異步,甚至可能根本和異步沒有關係(儘管幾乎沒有人會這樣做)-----比如直接在Beginxxx裡直接輸出一個"Helloworld",如果是這種極端的情況,那麼上面說的一切都是廢話,所以上面的探討並不涉及內部實現,只是告訴大家微軟的模式,和框架中對這個模式的經典實現)



異步和多線程有什麼關聯

有一句話總結的很好:多線程是實現異步的一種手段和工具

我們通常把多線程和異步等同起來,實際是一種誤解,在實際實現的時候,異步有許多種實現方法,我們可以用進程來做異步,或者使用纖程,或者硬件的一些特性,比如在實現異步IO的時候,可以有下面兩個方案:

1)可以通過初始化一個子線程,然後在子線程裡進行IO,而讓主線程順利往下執行,當子線程執行完畢就回調

2)也可以根本不使用新線程,而使用硬件的支持(現在許多硬件都有自己的處理器),來實現完全的異步,這是我們只需要將IO請求告知硬件驅動程序,然後迅速返回,然後等著硬件IO就緒通知我們就可以了

實際上DotNet Framework裡面就有這樣的例子,當我們使用文件流的時候,如果制定文件流屬性為同步,則使用BeginRead進行讀取時,就是用一個子線程來調用同步的Read方法,而如果指定其為異步,則同樣操作時就使用了需要硬件和操作系統支持的所謂IOCP的機制


WinForm多線程編程篇

 

我的多線程WinForm程序老是拋出InvalidOperationException ,怎麼解決?

在WinForm中使用多線程時,常常遇到一個問題,當在子線程(非UI線程)中修改一個控件的值:比如修改進度條進度,時會拋出如下錯誤

Cross-thread operation not valid: Control 'XXX' accessed from a thread other than the thread it was created on.

在VS2005或者更高版本中,只要不是在控件的創建線程(一般就是指UI主線程)上訪問控件的屬性就會拋出這個錯誤,解決方法就是利用控件提供的Invoke和BeginInvoke把調用封送回UI線程,也就是讓控件屬性修改在UI線程上執行,下面列出會報錯的代碼和他的修改版本


ThreadStart threadStart=new ThreadStart(Calculate);//通過ThreadStart委託告訴子線程講執行什麼方法
Thread thread=new Thread(threadStart);
thread.Start();
public void Calculate(){
    double Diameter=0.5;
    double result=Diameter*Math.PI;
    CalcFinished(result);
//計算完成需要在一個文本框裡顯示
}

public void CalcFinished(double result){
    this.TextBox1.Text=result.ToString();//會拋出錯誤
}

上面加粗的地方在debug的時候會報錯,最直接的修改方法是修改Calculate這個方法如下


delegate void changeText(double result);

public void Calculate(){
    double Diameter=0.5;
    double result=Diameter*Math.PI;
    this.BeginInvoke(new changeText(CalcFinished),t.Result);//計算完成需要在一個文本框裡顯示
}

這樣就ok了,但是最漂亮的方法是不去修改Calculate,而去修改CalcFinished這個方法,因為程序裡調用這個方法的地方可能很多,由於加了是否需要封送的判斷,這樣修改還能提高非跨線程調用時的性能


delegate void changeText(double result);

public void CalcFinished(double result){
    if(this.InvokeRequired){
        this.BeginInvoke(new changeText(CalcFinished),t.Result);
    }

    else{
        this.TextBox1.Text=result.ToString();
    }

}

上面的做法用到了Control的一個屬性InvokeRequired(這個屬性是可以在其他線程裡訪問的),這個屬性表明調用是否來自另非UI線程,如果是,則使用BeginInvoke來調用這個函數,否則就直接調用,省去線程封送的過程


Invoke,BeginInvoke幹什麼用的,內部是怎麼實現的?

這兩個方法主要是讓給出的方法在控件創建的線程上執行

Invoke使用了Win32API的SendMessage,

UnsafeNativeMethods.PostMessage(new HandleRef(this, this.Handle), threadCallbackMessage, IntPtr.Zero, IntPtr.Zero);

BeginInvoke使用了Win32API的PostMessage

UnsafeNativeMethods.PostMessage(new HandleRef(this, this.Handle), threadCallbackMessage, IntPtr.Zero, IntPtr.Zero);

這兩個方法向UI線程的消息隊列中放入一個消息,當UI線程處理這個消息時,就會在自己的上下文中執行傳入的方法,換句話說凡是使用BeginInvoke和Invoke調用的線程都是在UI主線程中執行的,所以如果這些方法裡涉及一些靜態變量,不用考慮加鎖的問題


每個線程都有消息隊列嗎?

不是,只有創建了窗體對象的線程才會有消息隊列(下面給出<Windows 核心編程>關於這一段的描述)

當一個線程第一次被建立時,系統假定線程不會被用於任何與用戶相關的任務。這樣可以減少線程對系統資源的要求。但是,一旦這個線程調用一個與圖形用戶界面有關的函數(例如檢查它的消息隊列或建立一個窗口),系統就會為該線程分配一些另外的資源,以便它能夠執行與用戶界面有關的任務。特別是,系統分配一個T H R E A D I N F O結構,並將這個數據結構與線程聯繫起來。

這個T H R E A D I N F O結構包含一組成員變量,利用這組成員,線程可以認為它是在自己獨佔的環境中運行。T H R E A D I N F O是一個內部的、未公開的數據結構,用來指定線程的登記消息隊列(posted-message queue)、發送消息隊列( send-message queue)、應答消息隊列( r e p l y -message queue)、虛擬輸入隊列(virtualized-input queue)、喚醒標誌(wake flag)、以及用來描述線程局部輸入狀態的若干變量。圖2 6 - 1描述了T H R E A D I N F O結構和與之相聯繫的三個線程。

 


為什麼Winform不允許跨線程修改UI線程控件的值

在vs2003下,使用子線程調用ui線程創建的控件的屬性是不會有問題的,但是編譯的時候會出現警告,但是vs2005及以上版本就會有這樣的問題,下面是msdn上的描述

"當您在 Visual Studio 調試器中運行代碼時,如果您從一個線程訪問某個 UI 元素,而該線程不是創建該 UI 元素時所在的線程,則會引發 InvalidOperationException。調試器引發該異常以警告您存在危險的編程操作。UI 元素不是線程安全的,所以只應在創建它們的線程上進行訪問"

從上面可以看出,這個異常實際是debugger耍的花招,也就是說,如果你直接運行程序的exe文件,或者利用運行而不調試(Ctrl+F5)來運行你的程序,是不會拋出這樣的異常的.大概ms發現v2003的警告對廣大開發者不起作用,所以用了一個比較狠一點的方法.

不過問題依然存在:既然這樣設計的原因主要是因為控件的值非線程安全,那麼DotNet framework中非線程安全的類千千萬萬,為什麼偏偏跨線程修改Control的屬性會有這樣嚴格的限制策略呢?

這個問題我還回答不好,希望博友們能夠予以補充

有沒有什麼辦法可以簡化WinForm多線程的開發

使用backgroundworker,使用這個組建可以避免回調時的Invoke和BeginInvoke,並且提供了許多豐富的方法和事件

參見.Net多線程總結(二)-BackgroundWorker,我在這裡不再贅訴


線程池

 

線程池的作用是什麼

作用是減小線程創建和銷毀的開銷

創建線程涉及用戶模式和內核模式的切換,內存分配,dll通知等一系列過程,線程銷毀的步驟也是開銷很大的,所以如果應用程序使用了完一個線程,我們能把線程暫時存放起來,以備下次使用,就可以減小這些開銷

所有進程使用一個共享的線程池,還是每個進程使用獨立的線程池?

每個進程都有一個線程池,一個Process中只能有一個實例,它在各個應用程序域(AppDomain)是共享的,.Net2.0 中默認線程池的大小為工作線程25個,IO線程1000個,有一個比較普遍的誤解是線程池中會有1000個線程等著你去取,其實不然, ThreadPool僅僅保留相當少的線程,保留的線程可以用SetMinThread這個方法來設置,當程序的某個地方需要創建一個線程來完成工作時,而線程池中又沒有空閒線程時,線程池就會負責創建這個線程,並且在調用完畢後,不會立刻銷毀,而是把他放在池子裡,預備下次使用,同時如果線程超過一定時間沒有被使用,線程池將會回收線程,所以線程池裡存在的線程數實際是個動態的過程

為什麼不要手動線程池設置最大值?

當我首次看到線程池的時候,腦袋裡的第一個念頭就是給他設定一個最大值,然而當我們查看ThreadPool的SetMaxThreads文檔時往往會看到一條警告:不要手動更改線程池的大小,這是為什麼呢?

其實無論FileStream的異步讀寫,異步發送接受Web請求,甚至使用delegate的beginInvoke都會默認調用 ThreadPool,也就是說不僅你的代碼可能使用到線程池,框架內部也可能使用到,更改的後果影響就非常大,特別在iis中,一個應用程序池中的所有 WebApplication會共享一個線程池,對最大值的設定會帶來很多意想不到的麻煩

線程池的線程為何要分類?

線程池有一個方法可以讓我們看到線程池中可用的線程數量:GetAvaliableThread(out workerThreadCount,out iocompletedThreadCount),對於我來說,第一次看到這個函數的參數時十分困惑,因為我期望這個函數直接返回一個整形,表明還剩多少線程,這個函數居然一次返回了兩個變量.

原來線程池裡的線程按照公用被分成了兩大類:工作線程和IO線程,或者IO完成線程,前者用於執行普通的操作,後者專用於異步IO,比如文件和網絡請求,注意,分類並不說明兩種線程本身有差別,線程就是線程,是一種執行單元,從本質上來講都是一樣的,線程池這樣分類,舉例來說,就好像某施工工地現在有1000把鐵鍬,規定其中25把給後勤部門用,其他都給施工部門,施工部門需要大量使用鐵鍬來挖地基(例子土了點,不過說明問題還是有效的),後勤部門用鐵鍬也就是鏟鏟雪,鏟鏟垃圾,給工人師傅修修臨時住房,所以用量不大,顯然兩個部門的鐵鍬本身沒有區別,但是這樣的劃分就為管理兩個部門的鐵鍬提供了方便

線程池中兩種線程分別在什麼情況下被使用,二者工作原理有什麼不同?

下面這個例子直接說明了二者的區別,我們用一個流讀出一個很大的文件(大一點操作的時間長,便於觀察),然後用另一個輸出流把所讀出的文件的一部分寫到磁盤上

我們用兩種方法創建輸出流,分別是

創建了一個異步的流(注意構造函數最後那個true)

FileStream outputfs=new FileStream(writepath, FileMode.Create, FileAccess.Write, FileShare.None,256,true);

創建了一個同步的流

FileStream outputfs = File.OpenWrite(writepath);

 然後在寫文件期間查看線程池的狀況

string readpath = "e:\\RHEL4-U4-i386-AS-disc1.iso";
string writepath = "e:\\kakakak.iso";
byte[] buffer = new byte[90000000];

//FileStream outputfs=new FileStream(writepath, FileMode.Create, FileAccess.Write, FileShare.None,256,true);
//Console.WriteLine("異步流");
//創建了一個同步的流

FileStream outputfs 
= File.OpenWrite(writepath);
Console.WriteLine(
"同步流");

 
//然後在寫文件期間查看線程池的狀況

ShowThreadDetail(
"初始狀態");

FileStream fs 
= File.OpenRead(readpath);

fs.BeginRead(buffer, 
090000000delegate(IAsyncResult o)
{

    outputfs.BeginWrite(buffer, 
0, buffer.Length,

    
delegate(IAsyncResult o1)
    
{

        Thread.Sleep(
1000);

        ShowThreadDetail(
"BeginWrite的回調線程");

    }
null);

    Thread.Sleep(
500);//this is important cause without this, this Thread and the one used for BeginRead May seem to be same one
}
,

null);


Console.ReadLine();

public static void ShowThreadDetail(string caller)
{
    
int IO;
    
int Worker;
    ThreadPool.GetAvailableThreads(
out Worker, out IO);
    Console.WriteLine(
"Worker: {0}; IO: {1}", Worker, IO);
}

輸出結果
異步流
Worker: 500; IO: 1000
Worker: 500; IO: 999
同步流
Worker: 500; IO: 1000
Worker: 499; IO: 1000
 

這兩個構造函數創建的流都可以使用BeginWrite來異步寫數據,但是二者行為不同,當使用同步的流進行異步寫時,通過回調的輸出我們可以看到,他使用的是工作線程,而非IO線程,而異步流使用了IO線程而非工作線程

其實當沒有制定異步屬性的時候,.Net實現異步IO是用一個子線程調用fs的同步Write方法來實現的,這時這個子線程會一直阻塞直到調用完成.這個子線程其實就是線程池的一個工作線程,所以我們可以看到,同步流的異步寫回調中輸出的工作線程數少了一,而使用異步流,在進行異步寫時,採用了 IOCP方法,簡單說來,就是當BeginWrite執行時,把信息傳給硬件驅動程序,然後立即往下執行(注意這裡沒有額外的線程),而當硬件準備就緒, 就會通知線程池,使用一個IO線程來讀取

.Net線程池有什麼不足

沒有提供方法控制加入線程池的線程:一旦加入線程池,我們沒有辦法掛起,終止這些線程,唯一可以做的就是等他自己執行

1)不能為線程設置優先級
2)一個Process中只能有一個實例,它在各個AppDomain是共享的。ThreadPool只提供了靜態方法,不僅我們自己添加進去的WorkItem使用這個Pool,而且.net framework中那些BeginXXX、EndXXX之類的方法都會使用此Pool。
3)所支持的Callback不能有返回值。WaitCallback只能帶一個object類型的參數,沒有任何返回值。
4)不適合用在長期執行某任務的場合。我們常常需要做一個Service來提供不間斷的服務(除非服務器down掉),但是使用ThreadPool並不合適。

下面是另外一個網友總結的什麼不需要使用線程池,我覺得挺好,引用下來
如果您需要使一個任務具有特定的優先級。
如果您具有可能會長時間運行(並因此阻塞其他任務)的任務。
如果您需要將線程放置到單線程單元中(所有 ThreadPool 線程均處於多線程單元中)。
如果您需要與該線程關聯的穩定標識。例如,您應使用一個專用線程來中止該線程、將其掛起或按名稱發現它。


鎖定與同步

CLR怎樣實現lock(obj)鎖定?

從原理上講,lock和Syncronized Attribute都是用Moniter.Enter實現的,比如如下