[.NET] 關於.NET線程問題總結(二)

來源:互聯網
上載者:User

經常看到名為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,並且提供了許多豐富的方法和事件

線程池
線程池的作用是什麼

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

建立線程涉及使用者模式和核心模式的切換,記憶體配置,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, 0, 90000000, delegate(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並不合適。

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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.