转:WINFORM多线程编程
把遇到過的對.Net線程的一些問題和誤解集中起來和大家分享,也希望大家能一起補充,熱烈歡迎討論
目錄
基礎篇
怎樣創建一個線程
受托管的線程與 Windows線程
前臺線程與后臺線程
名為BeginXXX和EndXXX的方法是做什么用的
異步和多線程有什么關聯
WinForm多線程編程篇
我的多線程WinForm程序老是拋出InvalidOperationException ,怎么解決?
Invoke,BeginInvoke干什么用的,內部是怎么實現的
每個線程都有消息隊列嗎?
為什么Winform不允許跨線程修改UI線程控件的值
有沒有什么辦法可以簡化WinForm多線程的開發
線程池
線程池的作用是什么?
所有進程使用一個共享的線程池,還是每個進程使用獨立的線程池?
為什么不要手動線程池設置最大值?
.Net線程池有什么不足?
同步
CLR怎樣實現lock(obj)鎖定?
WaitHandle是什么,他和他的派生類怎么使用
什么是用雙鎖實現Singleton,為什么要這樣做,為什么有人說雙鎖檢驗是不安全的
互斥對象(Mutex)、事件(Event)對象與lock語句的比較
什么時候需要鎖定
只有共享資源才需要鎖定
把鎖定交給數據庫
了解你的程序是怎么運行的
業務邏輯對事務和線程安全的要求
計算一下沖突的可能性
請多使用lock,少用Mutex
Web和IIS
應用程序池,WebApplication,和線程池之間有什么關系
Web頁面怎么調用異步WebService
基礎篇
怎樣創建一個線程
我只簡單列舉幾種常用的方法,詳細可參考.Net多線程總結(一)
一)使用Thread類
ThreadStartthreadStart=newThreadStart(Calculate);//通過ThreadStart委托告訴子線程講執行什么方法,這里執行一個計算圓周長的方法
Threadthread=newThread(threadStart);
thread.Start();//啟動新線程
publicvoidCalculate(){
doubleDiameter=0.5;
Console.Write("TheperimeterOfCirclewithaDiameterof{0}is{1}"Diameter,Diameter*Math.PI);
}
二)使用Delegate.BeginInvoke
delegatedoubleCalculateMethod(doubleDiameter);//申明一個委托,表明需要在子線程上執行的方法的函數簽名
staticCalculateMethodcalcMethod=newCalculateMethod(Calculate);//把委托和具體的方法關聯起來
staticvoidMain(string[]args)
{
//此處開始異步執行,并且可以給出一個回調函數(如果不需要執行什么后續操作也可以不使用回調)
calcMethod.BeginInvoke(5,newAsyncCallback(TaskFinished),null);
Console.ReadLine();
}
//線程調用的函數,給出直徑作為參數,計算周長
publicstaticdoubleCalculate(doubleDiameter)
{
returnDiameter*Math.PI;
}
//線程完成之后回調的函數
publicstaticvoidTaskFinished(IAsyncResultresult)
{
doublere=0;
re=calcMethod.EndInvoke(result);
Console.WriteLine(re);
}
三)使用ThreadPool.QueueworkItem
WaitCallbackw=newWaitCallback(Calculate);
//下面啟動四個線程,計算四個直徑下的圓周長
ThreadPool.QueueUserWorkItem(w,1.0);
ThreadPool.QueueUserWorkItem(w,2.0);
ThreadPool.QueueUserWorkItem(w,3.0);
ThreadPool.QueueUserWorkItem(w,4.0);
publicstaticvoidCalculate(doubleDiameter)
{
returnDiameter*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線程上執行,下面列出會報錯的代碼和他的修改版本
ThreadStartthreadStart=newThreadStart(Calculate);//通過ThreadStart委托告訴子線程講執行什么方法
Threadthread=newThread(threadStart);
thread.Start();
publicvoidCalculate(){
doubleDiameter=0.5;
doubleresult=Diameter*Math.PI;
CalcFinished(result);//計算完成需要在一個文本框里顯示
}
publicvoidCalcFinished(doubleresult){
this.TextBox1.Text=result.ToString();//會拋出錯誤
}
上面加粗的地方在debug的時候會報錯,最直接的修改方法是修改Calculate這個方法如下
delegatevoidchangeText(doubleresult);
publicvoidCalculate(){
doubleDiameter=0.5;
doubleresult=Diameter*Math.PI;
this.BeginInvoke(newchangeText(CalcFinished),t.Result);//計算完成需要在一個文本框里顯示
}
這樣就ok了,但是最漂亮的方法是不去修改Calculate,而去修改CalcFinished這個方法,因為程序里調用這個方法的地方可能很多,由于加了是否需要封送的判斷,這樣修改還能提高非跨線程調用時的性能
delegatevoidchangeText(doubleresult);
publicvoidCalcFinished(doubleresult){
if(this.InvokeRequired){
this.BeginInvoke(newchangeText(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);
然后在寫文件期間查看線程池的狀況
stringreadpath="e:\\RHEL4-U4-i386-AS-disc1.iso";
stringwritepath="e:\\kakakak.iso";
byte[]buffer=newbyte[90000000];
//FileStreamoutputfs=newFileStream(writepath,FileMode.Create,FileAccess.Write,FileShare.None,256,true);
//Console.WriteLine("異步流");
//創建了一個同步的流
FileStreamoutputfs=File.OpenWrite(writepath);
Console.WriteLine("同步流");
//然后在寫文件期間查看線程池的狀況
ShowThreadDetail("初始狀態");
FileStreamfs=File.OpenRead(readpath);
fs.BeginRead(buffer,0,90000000,delegate(IAsyncResulto)
{
outputfs.BeginWrite(buffer,0,buffer.Length,
delegate(IAsyncResulto1)
{
Thread.Sleep(1000);
ShowThreadDetail("BeginWrite的回調線程");
},null);
Thread.Sleep(500);//thisisimportantcausewithoutthis,thisThreadandtheoneusedforBeginReadMayseemtobesameone
},
null);
Console.ReadLine();
publicstaticvoidShowThreadDetail(stringcaller)
{
intIO;
intWorker;
ThreadPool.GetAvailableThreads(outWorker,outIO);
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實現的,比如如下代碼
objectlockobj=newobject();
lock(obj){
//dothings
}
在編譯時,會被編譯為類似
try{
Moniter.Enter(obj){
//dothings
}
}
catch{}
finally{
Moniter.Exit(obj);
}
而[MethodImpl(MethodImplOptions.Synchronized)]標記為同步的方法會在編譯時被lock(this)語句所環繞
所以我們只簡單探討Moniter.Enter的實現
(注:DotNet并非使用Win32API的CriticalSection來實現Moniter.Enter,不過他為托管對象提供了一個類似的結構叫做Syncblk)
每個對象實例頭部都有一個指針,這個指針指向的結構,包含了對象的鎖定信息,當第一次使用Moniter.Enter(obj)時,這個obj對象
的鎖定結構就會被初時化,第二次調用Moniter.Enter時,會檢驗這個object的鎖定結構,如果鎖沒有被釋放,則調用會阻塞
WaitHandle是什么,他和他的派生類怎么使用
WaitHandle是Mutex,Semaphore,EventWaitHandler,AutoResetEvent,ManualResetEvent共同的祖先,他們包裝了用于同步的內核對象,也就是說是這些內核對象的托管版本。
Mutex:類似于一個接力棒,拿到接力棒的線程才可以開始跑,當然接力棒一次只屬于一個線程(Thread Affinity),如果這個線程不釋放接力棒(Mutex.ReleaseMutex),那么沒辦法,其他所有需要接力棒運行的線程都知道能等著看熱鬧
Semaphore:類似于一個小桶,里面裝了幾個小球,凡是拿到小球就可以跑,比如指定小桶里最初有四個小球,那么開始的四個線程就可以直接拿著
自己的小球開跑,但是第五個線程一看,小球被拿光了,就只好乖乖的等著有誰放一個小球到小桶里(Semophore.Release),他才能跑,但是這
里的游戲規則比較特殊,我們可以隨意向小桶里放入小球,也就是說我可以拿走一個小球,放回去倆,甚至一個都不拿,放回去5個,這樣就有五個線程可以拿著這
些小球運行了.我們可以規定小桶里有開始有幾個小球(構造函數的第一個參數),也可以規定最多不能超過多少小球(構造函數的第二個參數)
ManualResetEvent,AutoResetEvent可以參考http://www.cnblogs.com/uubox/archive/2007/12/18/1003953.html
什么是用雙鎖實現Singleton,為什么要這樣做,雙鎖檢驗是不安全的嗎?
使用雙鎖檢驗技巧來實現單件,來自于Java社區
publicstaticMySingletonInstance{
get{
if(_instance!=null)}{
lock(_instance){
if(s_value==null){
_instance=newMySingleton();
}
}
}
}
}
這樣做其實是為了提高效率,比起
public static MySingleton Instance{
get{
lock(_instance){
if(svalue==null){
_instance= new MySingleton();
}
}
前一種方法在instance創建的時候不需要用lock同步,從而增進了效率
在java中這種技巧被證明是不安全的詳細見http://www.cs.umd.edu/~pugh/java/memoryModel/
但是在.Net下,這樣的技巧是成立的,因為.Net使用了改進的內存模型
并且在.Net下,我們可以使用LazyInit來實現單件
private static readonly _instance=new MySingleton()
public static MySingleton Instance{
get{return _instance}
}
當第一此使用_instance時,CLR會生成這個對象,以后再訪問這個字段,將會直接返回
互斥對象(Mutex),信號量(Semaphore),事件(Event)對象與lock語句的比較
首先這里所謂的事件對象不是System.Event,而是一種用于同步的內核機制
互斥對象和事件對象屬于內核對象,利用內核對象進行線程同步,線程必須要在用戶模式和內核模式間切換,所以一般效率很低,但利用互斥對象和事件對象這樣的內核對象,可以在多個進程中的各個線程間進行同步。
lock或者Moniter是.net用一個特殊結構實現的,不涉及模式切換,也就是說工作在用戶方式下,同步速度較快,但是不能跨進程同步
什么時候需要鎖定?
剛剛接觸鎖定的程序員往往覺得這個世界非常的危險,每個靜態變量似乎都有可能產生競爭
首先鎖定是解決競爭條件的,也就是多個線程同時訪問某個資源,造成意想不到的結果,比如,最簡單的情況,一個計數器,如果兩個線程同時加一,后果就是損失了一個計數,但是頻繁的鎖定又可能帶來性能上的消耗,還有最可怕的情況,死鎖
到底什么情況下我們需要使用鎖,什么情況下不用呢?
只有共享資源才需要鎖定
首先,只有可以被多線程訪問的共享資源才需要考慮鎖定,比如靜態變量,再比如某些緩存中的值,屬于線程內部的變量不需要鎖定
把鎖定交給數據庫
數據庫除了存儲數據之外,還有一個重要的用途就是同步,數據庫本身用了一套復雜的機制來保證數據的可靠和一致性,這就為我們節省了很多的精力.保證了數據源頭上的同步,我們多數的精力就可以集中在緩存等其他一些資源的同步訪問上了
了解你的程序是怎么運行的
實際上在web開發中大多數邏輯都是在單個線程中展開的,無論asp.net還是php,一個請求都會在一個單獨的線程中處理,其中的大部分變量都是屬于
這個線程的,根本沒有必要考慮鎖定,當然對于asp.net中的application對象中的數據,我們就要小心一些了
WinForm中凡是使用BeginInvoke和Invoke調用的方法也都不需要考慮同步,因為這用這兩個方法調用的方法會在UI線程中執行,因此實際是同步的,所以如果調用的方法中存在某些靜態變量,不需要考慮鎖定
業務邏輯對事務和線程安全的要求
這條是最根本的東西,開發完全線程安全的程序是件很費時費力的事情,在電子商務等涉及金融系統的案例中,許多邏輯都必須嚴格的線程安全,所以我們不得不犧
牲一些性能,和很多的開發時間來做這方面的工作,而一般的應用中,許多情況下雖然程序有競爭的危險,我們還是可以不使用鎖定,比如有的時候計數器少一多
一,對結果無傷大雅的情況下,我們就可以不用去管他
計算一下沖突的可能性
我以前曾經談到過,架構不要過設計,其實在這里也一樣,假如你的全局緩存里的某個值每天只有幾百或者幾千個訪問,并且訪問時間很短,并且分布均勻(實際上
這是大多數的情況),那么沖突的可能性就非常的少,也許每500天才會出現一次或者更長,從7*24小時安全服務的角度來看,也完全符合要求,那么你還會
為這樣萬分之一的可能性花80%的精力去設計嗎?
請多使用lock,少用Mutex
如果你一定要使用鎖定,請盡量不要使用內核模塊的鎖定機制,比如.net的
Mutex,Semaphore,AutoResetEvent,ManuResetEvent,使用這樣的機制涉及到了系統在用戶模式和內核模式間的切
換,所以性能差很多,但是他們的優點是可以跨進程同步線程,所以應該清楚的了解到他們的不同和適用范圍
Web和IIS
應用程序池,WebApplication,和線程池之間有什么關系
一個應用程序池是一個獨立的進程,擁有一個線程池,應用程序池中可以有多個WebApplication,每個運行在一個單獨的AppDomain中,這些WebApplication公用一個線程池
不同的AppDomain保證了每個WebApplication的靜態變量不會互相干擾,不同的應用程序池保證了一個網站癱瘓,其他不同進程中的站點還能正常運行
下圖說明了他們的關系
Web頁面怎么調用異步WebService
把Page的Async屬性設置為true,就可以調用異步的方法,但是這樣調用的效果可能并不如我們的相像,請參考Web中使用多線程來增強用戶體驗
推薦文章
http://www.cnblogs.com/uubox/archive/2007/12/18/1003953.html(內核對象同步,講的很通俗易懂ManuResetEvent,AutoResetEvent)
http://alang79.blogdriver.com/alang79/456761.html
A low-level Look at the ASP.NET Architecture
參考資料
<Windows 核心編程>這本書里對內核對象的描述比較詳盡
<.Net框架程序設計>和上面一本一樣也是大牛Jeffery Richard的作品
朱燚的技術博客,轉載請注明出處
http://yizhu2000.cnblogs.com
http://blog.csdn.net/yizhu2000
總結
以上是生活随笔為你收集整理的转:WINFORM多线程编程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Cocos2d-JS事件处理机制
- 下一篇: Java与.NET的WebService