【基礎篇】
- 怎樣創建一個線程
- 受托管的線程與Windows線程
- 前台線程與后台線程
- 名為BeginXXX和EndXXX的方法是做什么用的
- 異步和多線程有什么關聯
【WinForm多線程編程篇】
- 多線程WinForm程序總是拋出InvalidOperationException,怎么解決
- Invoke和BeginInvoke干什么用的,內部是怎么實現的
- 每個線程都有消息隊列嗎
- 為什么WinForm不允許跨線程修改UI線程控件的值
- 有沒有什么辦法可以簡化WinForm多線程的開發
【線程池】
- 線程池的作用是什么
- 所有進程使用一個共享的線程池,還是每個進程使用獨立的線程池
- 線程池中線程的分類
- .NET線程池有什么不足
【同步】
- CLR怎樣實現lock(obj)鎖定
- 互斥對象(Mutex)、事件(Event)對象與lock語句的比較
基礎篇
怎樣創建一個線程
方法一:使用Thread類
public static void Main(string[] args)
{
//方法一:使用Thread類
ThreadStart threadStart = new ThreadStart(Calculate);//通過ThreadStart委托告訴子線程執行什么方法 Thread thread = new Thread(threadStart);
thread.Start();//啟動新線程
}
public static void Calculate()
{
Console.Write("執行成功");
Console.ReadKey();
}
方法二:使用Delegate.BeginInvoke
delegate double CalculateMethod(double r);//聲明一個委托,表明需要在子線程上執行的方法的函數簽名
static CalculateMethod calcMethod = new CalculateMethod(Calculate);
static void Main(string[] args)
{
//方法二:使用Delegate.BeginInvoke
//此處開始異步執行,並且可以給出一個回調函數(如果不需要執行什么后續操作也可以不使用回調)
calcMethod.BeginInvoke(5, new AsyncCallback(TaskFinished), null);
Console.ReadLine();
}
public static double Calculate(double r)
{
return 2 * r * Math.PI;
}
//線程完成之后回調的函數
public static void TaskFinished(IAsyncResult result)
{
double re = 0;
re = calcMethod.EndInvoke(result);
Console.WriteLine(re);
}
方法三:使用ThreadPool.QueueworkItem
受托管的線程與Windows線程
.NET應用的線程實際上仍然是Windows線程。但是,當某個線程被CLR所知時,我們將它稱為受托管的線程。具體來說,由受托管的代碼創建出來的線程就是受托管的線程。不過,一旦該線程執行了受托管的代碼它就變成了受托管的線程。
一個受托管的線程和非受托管的線程的區別在於,CLR將創建一個System.Threading.Thread類的實例來代表並操作前者。在內部實現中,CLR將一個包含了所有受托管線程的列表保存在一個叫做ThreadStore地方。
CLR確保每一個受托管的線程在任意時刻都在一個AppDomain中執行,但是這並不代表一個線程將永遠處在一個AppDomain中,它可以隨着時間的推移轉到其他的AppDomain中。
前台線程與后台線程
啟動了多個線程的程序在關閉的時候卻出現了問題,如果程序退出的時候不關閉線程,那么線程就會一直的存在,但是大多啟動的線程都是局部變量,不能一一的關閉,如果調用Thread.CurrentThread.Abort()方法關閉主線程的話,就會出現ThreadAbortException異常。可以通過這個方法:Thread.IsBackground設置線程為后台線程。
msdn對前台線程和后台線程的解釋:托管線程或者是后台線程,或者是前台線程。后台線程不會是托管執行環境處於活動狀態,除此之外,后台線程與前台線程是一樣的。一旦所有前台進程在托管進程(其中.exe文件時托管程序集)中被停止,系統將停止所有后台線程並關閉。通過設置Thread.IsBackground屬性,可以將一個線程指定為后台線程或者前台線程。從非托管代碼進入托管執行環境的所有線程都被標記為后台線程。通過創建並啟動新的Thread對象而生成的所有線程都是前台線程。
名為BeginXXX和EndXXX的方法是做什么用的
這是.net的一個異步方法名稱規范。
.net在設計的時候為異步編程設計了一個異步編程模型(APM),比如所有的Stream就是BeginRead,EndRead,Socket,WebRequet,SqlCommand都運用到了這個模式,一般來講,調用BeginXXX的時候,一般會啟動一個異步過程去執行一個操作,EndInvoke可以接受這個異步操作的返回,當然如果異步操作在EndIncoke調用的時候還沒有執行完成,EndInvoke會一直等待異步操作完成或者超時。
.NET的異步編程模型(APM)一般包含BeginXXX,EndXXX,IAsyncResult這三個元素,BeginXXX方法都有返回一個IAsyncResult,而EndXXX都需要接受一個IAsyncResult作為參數。
異步和多線程
異步有許多種方法,我們可以用進程來做異步,或者使用線程,或者硬件的一些特性,比如在實現異步IO的時候,可以以下兩種方案:
方案一:可以通過初始化一個子線程,然后在子線程里進行IO,而讓主線程順利往下執行,當子線程執行完畢就回調
方案二:使用硬件的支持(現在許多硬件都有自己的處理器),來實現完全的異步,這時我們只需將IO請求告知硬件驅動程序,然后迅速返回,然后等着硬件IO就緒通知我們就可以了
WinForm多線程編程篇
多線程WinForm程序總是拋出InvalidOperationException,怎么解決
在WinForm中使用線程時,常常遇到一個問題,當在子線程(非UI線程)中修改一個空間的值:比如修改進度條進度,時會拋出異常。
解決方法就是利用控件提供的Invoke和BeginInvoke把調用封送回UI線程,也就是讓控件屬性修改在UI線程上執行。
例如:
delegate void changeText(double result);
public Form1()
{
InitializeComponent();
ThreadStart threadStart = new ThreadStart(Calculate);
Thread thread = new Thread(threadStart);
thread.Start();
}
public void Calculate()
{
double r = 2;
double result = 2 * Math.PI * r;
CalcFinished(result);
}
public void CalcFinished(double result)
{
if (this.InvokeRequired)
{
this.BeginInvoke(new changeText(CalcFinished), result);
}
else
{
this.textBox1.Text = result.ToString();
}
}
這里用到了Control的一個屬性InvokeRequired(這個屬性石可以在其它線程里訪問),這個屬性表明調用是否來自非UI線程,如果是,使用BeginInvoke來調用這個函數,否則就直接調用,省去線程封送的過程。
Invoke和BeginInvoke干什么用的,內部是怎么實現的
這兩個方法主要是讓給出的方法在控件創建的線程上執行。
Invoke使用了Win32API的SendMessage BeginInvoke使用了Win32API的PostMessage
這兩個方法想UI線程的消息隊列中放入一個消息,當UI線程處理這個消息時,就會在自己的上下文中執行傳入的方法,換句話說,凡是使用BeginInvoke和Invoke調用的線程都是在UI主線程中執行,所以如果這些方法里涉及一些靜態變量,不用考慮加鎖的問題。
每個線程都有消息隊列嗎?
不是,知識創建了窗體對象的線程才會有消息隊列(下面是《Windows核心編程》關於這一段的描述)
當一個線程第一希被建立時,系統假定線程不會被用於任何與用戶相關的任務。這樣可以減少線程對系統資源的要求。但是,一旦這個線程調用一個與圖形用戶界面有關的函數(例如檢查它的消息隊列或建立一個窗口),系統就會為該線程分配一些另外的資源,以便它能夠執行與用戶界面有關的任務。特別是,系統分配一個THREADINFO結構,並將這個數據結構與線程聯系起來。
這個THREADINFO結構包含一組成員變量,利用這組成員,線程可以認為它是在自己獨占的環境中運行。THREADINFO是一個內部的、未公開的數據結構,用來指定線程的登記消息隊列(posted-message queue)、發送消息隊列(send-message queue)、應答消息隊列(reply-message queue)、虛擬輸入隊列(virtualized-input queue)、喚醒標志(wake flag)以及用來描述線程局部輸入狀態的若干變量。
為什么WinForm不允許跨線程修改UI線程控件的值
vs2005及以上版本,當在Visual Studio調試器中運行代碼時,如果您從一個線程訪問某個UI元素,而該線程不是創建該UI元素時所在的線程,則會引發InvalidOperationException調試器引發該異常以警告您存在危險的編程操作。UI元素不是線程安全的,所以只應在創建它們的線程上進行訪問。
有沒有什么辦法可以簡化WinForm多線程的開發
使用backgroundworker,使用這個組件可以避免回調時的Invoke和BeginInvoke,並且提供了許多豐富的方法和事件
線程池
線程池的作用是什么
減小線程創建和銷毀的開銷
創建線程涉及到用戶模式和內核模式的切換,內存分配,dll通知等一系列過程,線程銷毀的步驟也是開銷很大的,所以如果應用程序使用完一個線程,我們能把線程暫時存放起來,以備下次使用,就可以減小這些開銷。
所有進程使用一個共享的線程池,還是每個進程使用獨立的線程池
每個進程都有一個線程池,一個進程中只能有一個實例,它在各個應用程序域(AppDomain)是共享的,線程池僅僅保留相當少的線程,保留的線程可以用SetMinThread這個方法設置,當程序需要一個線程時,線程池中沒有空閑的線程時,線程池就會負責創建這個線程,調用完后,不會立即銷毀,而是把它放在池子里,以備下次使用,但是,如果超出一定時間沒使用,線程池就會回收線程,所以線程池里存在的線程數實際是個動態的過程。
線程池中線程的分類
線程池里的線程按照公用被分成了兩大類:工作線程和IO線程(IO完成線程),前者用於執行普通操作,后者專用於異步IO。它們分別在什么情況下被使用,二者工作原理有什么不同?通過下面這個例子,我們用一個流讀出一個很大的文件(文件大,操作時間長,便於觀察),然后用另一個輸出流把所讀出的文件的一部分寫到磁盤上。
用兩種方法創建輸出流,分別是:
創建一個異步的流(注意構造函數最后那個true)
創建一個同步流
string readPath = "d:\\工作常用軟件\\VS2012Documentation.iso";
string writePath = "d:\\vs2012.ios";
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);
},
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:1023; IO:1000
Worker:1023; IO:999
同步流
Worker:1023; IO:1000
Worker:1022; IO:1000
這兩個構造函數創建的流都可以使用BeginWrite來異步寫數據,但二者行為不同,當使用同步的流進行異步寫時,通過回調的輸出我們可以看到,它使用的是工作線程,而非IO線程,而異步流使用IO線程。
.NET線程池有什么不足
沒有提供方法控制加入線程池的線程:一旦加入線程池,我們沒辦法掛起,終止這些線程,唯一可以做的就是等他自己執行
- 不能為線程設置優先級
- 所支持的Callback不能有返回值。WaitCallback只能帶一個object類型的參數
- 不適合用於長期執行某任務的場合
同步
CLR怎樣實現lock(obj)鎖定
從原理上講,lock和Syncronized Attribute都是用Moniter.Enter實現的,例如:
object obj = new object();
lock(obj){
//do things...
}
在編譯時,會被編譯為類似
try{
Moniter.Enter(obj){
//do things...
}
}
catch{...}
finally{
Moniter.Exit(obj);
}
每個對象實例頭部都有一個指針,這個指針指向的結構包含了對象的鎖定信息,當第一次使用Moniter.Enter(obj)是,這個obj對象的鎖定結構就會被初始化,第二次調用時,會檢驗這個object的鎖定結構,如果鎖沒有被釋放,則調用會阻塞。
互斥對象(Mutex)、事件(Event)對象與lock語句的比較
這里所謂的事件是一種用於同步的內核機制,互斥對象和事件對象屬於內核對象,利用內核對象進行線程同步,線程必須要在用戶模式和內核模式間切換,所有一般效率很低,但利用互斥對象和事件對象這樣的內核對象,可以在多個線程中的各個線程間進行同步。
lock或者Moniter是.net用於一種特殊結構實現的,不涉及模式切換,就是工作在用戶方式下,同步速度較快,但是不能跨進程同步。