作者:zlbcdn
現狀
使用ThreadPool的QueueUserWorkItem方法完成異步操作會(huì )存在兩個(gè)問(wèn)題:
1、系統無(wú)法知道異步操作是否完成
2、無(wú)法獲取異步操作完成時(shí)的返回值
問(wèn)題來(lái)了,那就需要新的解決方案(忽然想起上《通信原理》時(shí)老師講的話(huà),“遇到問(wèn)題,解決問(wèn)題,因此就有了不同的編碼方式”,從調幅,到調頻,再到碼分….,工程領(lǐng)域的主題就是遇到問(wèn)題,解決問(wèn)題!跑題了?。?
為了解決上面提到的問(wèn)題,.NET提出了Task的概念
Task
Task的構造方法如下圖所示:
Task<TResult>類(lèi),其就是代表一個(gè)可以有返回值的異步操作。 public delegate TResult Func<out TResult>()具體的例子如下:
假設,操作的代碼如下:
private static int Sum(int n){ int sum=0; for(;n>0;n--) checked{sum += n;} return sum;}若程序希望獲取操作的返回值,則應該如此使用:
Task<int> t = new Task<int>(n=>Sum((int)n),1000000);t.start();t.wait(); //可以寫(xiě)這句,也可以不寫(xiě)Console.WriteLine(t.Result);在.NET中有兩個(gè)Task類(lèi),一個(gè)是不返回參數的,其參數中使用的是Action委托
可返回結果的Task,其參數是使用的Func<Object,TResult>或者是不帶參數的Func<TResult>
Task的異常
在Task的異步操作中可能會(huì )出現異常,若是使用Task<TResult>,在調用其Wait或Result方法時(shí),異常就會(huì )拋出。若使用的Task,則可以調用wait方法,異常就會(huì )拋出。另外,不管是Task<TResult>還是Task,只要查詢(xún)其Exception屬性,異常也會(huì )拋出。
Task的異常會(huì )被存儲在一個(gè)異常集合中,其名稱(chēng)為AggregateException。其內部有一個(gè)InnerExceptions 的屬性,其定義如下:
public ReadOnlyCollection<Exception> InnerExceptions { get; }通過(guò)定義可以看到其返回結果是一個(gè)異常的集合類(lèi)。因此,可以通過(guò)for循環(huán),完成對每一個(gè)異常的處理
AggregateException還有一個(gè)Handle方法,該方法的定義如下:
public void Handle(Func<Exception,?bool> predicate)(寫(xiě)到這兒,不得不發(fā)出感慨!當初設計.NET的這幫人真他媽厲害?。?
這個(gè)方法的作用就是為AggregateException內包含的每一個(gè)異常都調用一個(gè)回調方法
舉例說(shuō)明:
using System;using System.IO;using System.Threading.Tasks;public class Example{ public static void Main() { // This should throw an UnauthorizedAccessException. try { var files = GetAllFiles(@"C:\"); if (files != null) foreach (var file in files) Console.WriteLine(file); } catch (AggregateException ae) { //這兒就是一個(gè)遍歷InnerExceptions循環(huán),將內部所有的異常全部遍歷,然后進(jìn)行相應處理 //下面的代碼比較簡(jiǎn)單,可通過(guò)判斷是個(gè)什么異常,從而進(jìn)行下一步的相關(guān)操作 foreach (var ex in ae.InnerExceptions) //例如: //if(ex is UnauthorizedAccessException) // doSomeThing(); Console.WriteLine("{0}: {1}", ex.GetType().Name, ex.Message); } Console.WriteLine(); // This should throw an ArgumentException. try { foreach (var s in GetAllFiles("")) Console.WriteLine(s); } catch (AggregateException ae) { foreach (var ex in ae.InnerExceptions) Console.WriteLine("{0}: {1}", ex.GetType().Name, ex.Message); } } static string[] GetAllFiles(string path) { var task1 = Task.Run( () => Directory.GetFiles(path, "*.txt", SearchOption.AllDirectories)); try { return task1.Result; } catch (AggregateException ae) { //Handle是傳入一個(gè)異常參數,返回一個(gè)bool的結果,若處理則返回true,否則返回false //Handle方法應該是一個(gè)遍歷方法,即通過(guò)InnerExceptions屬性,為每一個(gè)異常添加這個(gè)回調方法 ae.Handle( x => { // Handle an UnauthorizedAccessException if (x is UnauthorizedAccessException) { Console.WriteLine("You do not have permission to access all folders in this path."); Console.WriteLine("See your network administrator or try another path."); } return x is UnauthorizedAccessException; }); return Array.Empty<String>(); } }}Task的方法關(guān)于阻塞
Task類(lèi)中有幾個(gè)Wait方法:Wait、WaitAny、WaitAll。具體可參考MSDN的方法說(shuō)明:具體的鏈接在這兒,這兒說(shuō)明的是:
不管是哪個(gè)方法,調用這三者方法的任何一個(gè),都會(huì )造成調用線(xiàn)程被阻塞,即等待task完成相關(guān)的操作。
這兒就有一個(gè)區別,之前join方法與wait方法同樣都是讓線(xiàn)程等待,但是內部如何實(shí)現及兩者的區別
.NET中join與wait方法的區別
在.NET中join方法的源碼如下:(查看.NET的源碼的網(wǎng)址在這兒:查看.NET的源碼網(wǎng)址)
//Join方法的源代碼[SecuritySafeCritical, HostProtection(SecurityAction.LinkDemand, Synchronization=true, ExternalThreading=true)]public void Join(){ this.JoinInternal();}JoinInternal的源代碼未知,只知是window內核的方法
[MethodImpl(MethodImplOptions.InternalCall), SecurityCritical, HostProtection(SecurityAction.LinkDemand, Synchronization=true, ExternalThreading=true)]private extern void JoinInternal();而wait方法的代碼如下:
//其他的無(wú)關(guān)代碼,關(guān)鍵的一行如下:Thread.SpinWait(Environment.ProcessorCount * (((int) 4) << i));而SpinWait是也是window內核的方法,但是通過(guò)名稱(chēng)可以得知,其是一個(gè)自旋的等待。即代表占用CPU的資源,使得線(xiàn)程被掛起。
個(gè)人對join方法的理解,如下圖所示:
t.Wait()語(yǔ)句時(shí), 調用線(xiàn)程并不是就傻傻的被阻塞了,而是先去看看當前線(xiàn)程是否開(kāi)始執行,若當前線(xiàn)程尚未開(kāi)始執行操作,則調用線(xiàn)程就會(huì )將當前線(xiàn)程將要執行的操作加載到調用線(xiàn)程中執行;若當前線(xiàn)程的操作已經(jīng)開(kāi)始執行了,那沒(méi)辦法,調用線(xiàn)程只能被阻塞。 //構造一個(gè)取消操作對象private CancellationTokenSource cts = new CancellationSource();public static void Main(string[] args){ //定義一個(gè)具有返回結果類(lèi)型的Task<TResult> Task<int> t=new Task<int>(tempMethod,t.Token); t.start(); //取消操作 cts.Cancel(); try { Console.WriteLine("返執行結果是:"+t.Result); } catch(AggregateException ex) //只要是使用了Task,則catch捕獲的異常就應該使用AggregatException,而不是簡(jiǎn)單的使用Exception { ex.Handle(e=>e is OperationCanceledException); Console.WriteLine("Sum操作已經(jīng)完成") }}//要符合Func委托private static int tempMethod(){ int resultInt=Sum(cts.Token,10000);}private static int Sum(CancellationToken ct,int n){ int sum=0; for(;n>0;n--) { //若請求操作取消操作,則拋出OperationCanceledException ct.ThrowIfCancellationRequest(); checked{sum+=n;} } return sum;}當任務(wù)完成后,自動(dòng)執行下一個(gè)任務(wù)
通過(guò)wait方法或者是task.Result屬性獲取操作最后的結果,這個(gè)是存在問(wèn)題的。因為這樣做會(huì )阻塞線(xiàn)程(Result屬性的內部就是調用了wait方法)。因此為了避免因阻塞而導致的性能問(wèn)題,.NET提供了一種回調機制,當線(xiàn)程完成操作時(shí),就會(huì )調用callback方法。
特別說(shuō)明,調用callback的線(xiàn)程是一個(gè)新的線(xiàn)程。
而實(shí)現callback的就是ContinueWith方法,ContinueWith方法的定義如下,更多的定義參見(jiàn):MSDN中方法的定義
public Task ContinueWith( Action<Task, object> continuationAction, object state, CancellationToken cancellationToken, TaskContinuationOptions continuationOptions, TaskScheduler scheduler)2、一個(gè)Task對象內部包含了一個(gè)ContinueWith的對象集合。即一個(gè)task對象可以聲明多個(gè)繼續的操作。例如:
Task t = new Task(Action()); t.ContinueWith(task=>Console.WriteLine("操作1:Task的結果是"+t.Result)); t.ContinueWith(task=>Console.WriteLine("操作2:Task的結果是"+t.Result)); t.ContinueWith(task=>Console.WriteLine("操作3:Task的結果是"+t.Result));當Task的操作完成后,線(xiàn)程池中會(huì )啟用3個(gè)線(xiàn)程處理相應的回調方法。
3、ContinueWith方法中有一參數:TaskContinuationOptions,是指存在某些情況的下才調用,具體參見(jiàn)參數。一般而言,continuewith是通過(guò)創(chuàng )建一個(gè)獨立的task完成回調方法的調用,但是這個(gè)選項中也可以有一個(gè)AttachedToParent選項,使其成為一個(gè)Task的子任務(wù)
創(chuàng )建子任務(wù)
因為task的構造器中只是定義了委托的定義,但沒(méi)有規定符合委托的方法中是否可以創(chuàng )建task。因此可以在task內部創(chuàng )建子任務(wù),但是在創(chuàng )建子任務(wù)的時(shí)候,需要明確子任務(wù)的創(chuàng )建方式,若是正常創(chuàng )建,則新任務(wù)將是獨立的task。想成為task的子任務(wù),可以在task的構造器中使用TaskCreationOptions.AttachedToParent屬性。這樣就相當于子任務(wù)與父任務(wù)綁定在一起,只有當所有的任務(wù)完成時(shí),父任務(wù)才認為是完成了。舉例如下:
//在定義的時(shí)候同步創(chuàng )建了3個(gè)子線(xiàn)程,并且用了TaskCreationOptions.AttachedToParent屬性Task<int[]> parent = new Task<int[]>( int[] results=new int[3]; new Task(()=>results[0]=Sum(100),TaskCreationOptions.AttachedToParent).start(); new Task(()=>results[0]=Sum(200),TaskCreationOptions.AttachedToParent).start(); new Task(()=>results[0]=Sum(300),TaskCreationOptions.AttachedToParent).start(); return results;);var ctw = parent.ContinueWith( parentTask=> Array.ForEach(Console.WriteLine("結果為:"+parentTask.Result)););parent.Start(); 任務(wù)內部/小節
1、Task所使用的線(xiàn)程默認是創(chuàng )建新線(xiàn)程,而不是使用線(xiàn)程池線(xiàn)程。因此,其資源的占用和消耗要大于ThreadPool.QueueUserWorkItem
2、Task實(shí)現了IDispose接口,因此在用完task時(shí)要調用dispose,以釋放資源。而不要使用GC回收
3、每個(gè)Task有唯一ID,可通過(guò)CurrentID獲取,而CurrentID是一個(gè)可控類(lèi)型的Int32
4、Task在生命周期中會(huì )有幾個(gè)狀態(tài):
4.1 Createed //任務(wù)顯示的創(chuàng )建完??墒褂胹tart方法,手動(dòng)開(kāi)始這個(gè)任務(wù)
4.2 WaitingForActivation //任務(wù)隱式創(chuàng )建。會(huì )自動(dòng)開(kāi)始。例如,通過(guò)continuewith開(kāi)始的任務(wù)
4.3 WaitingToRun //已經(jīng)進(jìn)入調度,尚未開(kāi)始
4.4 Runing
4.5 WaitingForChildrenToComplete
4.6 task最終的結果為:
4.6.1 RanToCompletion
4.6.2 Cancelled
4.6.3 Faulted
5、可以通過(guò)task的Status屬性獲取task的狀態(tài)。同時(shí)Task提供了幾個(gè)屬性:
IsCanceled、IsCompleted、IsFaulted判斷task的狀態(tài)。但是有一個(gè)特殊情況:當Task的狀態(tài)為RanToCompletion、Cancelled、Faulted中的任意狀態(tài)時(shí),調用Task的IsCompleted屬性,其都將返回true。因此判斷一個(gè)task是正常的完成的方式是:
if(task.Status==TaskStatus.RanToCompletion){ //.....}任務(wù)工廠(chǎng)
有時(shí)候會(huì )遇到一種情況,用相同的配置創(chuàng )建多個(gè)Task。一種方法是挨個(gè)創(chuàng )建task;而另外一個(gè)方法就是使用任務(wù)工廠(chǎng)。(個(gè)人認為任務(wù)工廠(chǎng)使用的機會(huì )不會(huì )很多)
任務(wù)工廠(chǎng)同Task,也有兩種方式,TaskFactory、TaskFactory<TResult>。
可以通過(guò)TaskFactory的構造器創(chuàng )建TaskFactory實(shí)例,也可以使用Task類(lèi)的TaskFactory屬性進(jìn)行創(chuàng )建,一般情況是使用后者。
TaskFactory具體的使用方法,可以參考MSDN的資料:TaskFactory
任務(wù)調度器
1、.NET中有多個(gè)類(lèi)型的任務(wù)調度器,其中線(xiàn)程池使用的是“線(xiàn)程池線(xiàn)程任務(wù)調度器”,而GUI(WinForm、WPF、SilverLight)使用的則是“同步上下文任務(wù)調度器”,而通過(guò)不同的任務(wù)調度器所創(chuàng )建的task,其不允許相互操作。因此使用“線(xiàn)程池線(xiàn)程任務(wù)調度器”所創(chuàng )建的線(xiàn)程,不能操作界面(改變標題之類(lèi)的操作),否則就會(huì )爆出InvalidOperationException
但是若希望線(xiàn)程池中的操作可以操作GUI上的元素,則其需通過(guò)TaskScheduler.FromCurrentSynchronizationContext()方法獲得“同步上下文任務(wù)調度器”,在創(chuàng )建task時(shí),將其作為參數傳入。書(shū)中的例子很好,如下:
internal sealed class MyFrom:Form{ //Form的構造器,初始化標題等內容 public MyFrom(){ this.text="同步上下文任務(wù)調度器demo"; visible=true; width=400; height=400; } //通過(guò)TaskScheduler獲得當前Form的“同步上下文任務(wù)調度器” private readonly TaskScheduler m_syncContextTaskScheduler= TaskScheduler.FromCurrentSynchronizationContext(); private CancellationTokeSource cts; //重寫(xiě)鼠標的點(diǎn)擊方法 protected override void OnMouseClick(MouseEventArgs e){ if(cts!=null){ cts.cancel(); cts=null; }else{ text="操作開(kāi)始"; cts = new CancellationTokeSource(); //下面的這個(gè)task使用線(xiàn)程池的任務(wù)調度器,也就是默認的任務(wù)調度器 var task = new task(()=>Sum(cts.Token,2000),cts.Token); task.ContinueWith( t=>Text="結果為"+t.Result, CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion, syncContextTaskScheduler ); task.ContinueWith( t=>Text="操作被取消", CancellationToken.None, TaskContinuationOptions.OnlyOnCanceled, syncContextTaskScheduler ); task.ContinueWith( t=>Text="操作失敗", CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, syncContextTaskScheduler ); }//else結束 base.OnMouseClick(e); }}代碼簡(jiǎn)單易懂,有一點(diǎn)很有意思。就是傳入的參數中有:CancellationToken.None,這個(gè)很有意思。其本質(zhì)的想法就是,我創(chuàng )建的這個(gè)操作,不想被外界的cancel方法所取消,但是方法中還必須有一個(gè)這個(gè)參數,因此就使用CancellationToken.None。這個(gè)CancellationToken.None會(huì )返回一個(gè)CancellationToken,因其與任何的CancellationTokenSource沒(méi)有任何關(guān)系,因此操作也就不能被取消了
26.4讀書(shū)思考
1、task還是挺耗費資源的,但是又比Thread、ThreadPool等好用,沒(méi)辦法只能使用它
2、在26.4遇到一個(gè)問(wèn)題,join與wait方法區別,有待解答!
聯(lián)系客服