【转】C#与C++的发展历程第一 - 由C#3.0起
C#5.0作為第五個C#的重要版本,將異步編程的易用度推向一個新的高峰。通過新增的async和await關鍵字,幾乎可以使用同編寫同步代碼一樣的方式來編寫異步代碼。
本文將重點介紹下新版C#的異步特性以及部分其他方面的改進。同時也將介紹WinRT程序一些異步編程的內容。
?
C# async/await異步編程
寫async異步編程這部分內容之前看了好多文章,反復整理自己的思路,盡力保證文章的正確性。盡管如此仍然可能存在錯誤,請廣大園友及時指出,感謝感謝。
異步編程不是一個新鮮的話題,最早期的C#版本也內建對異步編程的支持,當然在顏值上無法與目前基于TAP,使用async/await的異步編程相比。異步編程要解決的問題就是許多耗時的IO可能會阻塞線程導致CPU空轉降低效率,或者一個長時間的后臺任務會阻塞用戶界面。通過將耗時任務異步執行來使系統有更高的吞吐量,或保持界面的響應能力。例如界面在加載一幅來自網絡的圖像時,還可以及時響應用戶進行的其他操作。
按前文慣例先上一張圖通覽一下TAP模式下異步編程的方方面面,然后由異步編程的發展來討論一下TAP異步模式。
圖1
APM
C# .NET最早出現的異步編程模式被稱為APM(Asynchronous Programming Model)。這種模式主要由一對Begin/End開頭的組成。BeginXXX方法用于啟動一個耗時操作(需要異步執行的代碼段),相應的調用EndXXX來結束BeginXXX方法開啟的異步操作。BeginXXX方法和EndXXX方法之間的信息通過一個IAsyncResult對象來傳遞。這個對象是BeginXXX方法的返回值。如果直接調用EndXXX方法,則將以阻塞的方式去等待異步操作完成。另一種更好的方法是在BeginXXX倒數第二個參數指定的回調函數中調用EndXXX方法,這個回調函數將在異步操作完成時被觸發,回調函數的第二個參數即是EndXXX方法所需要的IAsyncResult對象。
.NET中一個典型的例子如System.Net命名空間中的HttpWebRequest類里的BeginGetResponse和EndGetResponse這對方法:
IAsyncResult?BeginGetResponse(AsyncCallback?callback,?object?state) WebResponse?EndGetResponse(IAsyncResult?asyncResult)由方法聲明即可看出,它們符合前述的模式。
APM使用簡單明了,雖然代碼量稍多,但也在合理范圍之內。APM兩個最大的缺點是:1.不支持進度報告 2.不能方便的“取消”。
?
EAP
在C#?.NET第二個版本中,增加了一種新的異步編程模型EAP(Event-based Asynchronous Pattern),EAP模式的異步代碼中,典型特征是一個Async結尾的方法和Completed結尾的事件。XXXCompleted事件將在異步處理完成時被觸發,在事件的處理函數中可以操作異步方法的結果。往往在EAP代碼中還會存在名為CancelAsync的方法用來取消異步操作,以及一個ProgressChanged結尾的事件用來匯報操作進度。通過這種方式支持取消和進度匯報也是EAP比APM更有優勢的地方。通過后文TAP的介紹,你會發現EAP中取消機制沒有可延續性,并且不是很通用。
.NET2.0中新增的BackgroundWorker可以看作EAP模式的一個例子。另一個使用EAP的例子是被HttpClient所取代的WebClient類(新代碼應該使用HttpClient而不是WebClient)。WebClient類中通過DownloadStringAsync方法開啟一個異步任務,并有DownloadStringCompleted事件供設置回調函數,還能通過CancelAsync方法取消異步任務。
?
TAP & async/await
從.NET4.0開始新增了一個名為TPL的庫主要負責異步和并行操作的處理,目標就是使異步和并發操作有個統一的操作界面。TPL庫的核心是Task類,有了Task幾乎不用像之前版本的異步和并發那樣去和Thread等底層類打交道,作為使用者的我們只需要處理好Task,Task背后有一個名為的TaskScheduler的類來處理Task在Thread上的執行。可以這樣說TaskScheduler和Task就是.NET4.0中異步和并發操作的基礎,也是我們寫代碼時不二的選擇。
對于Task可以將其理解為一個包裝委托對象(通常就是Action或Func對象)并執行的容器,從Task對象的創建就可以看出:
Action?action?=?()?=>?Console.WriteLine("Hello?World"); Task?task1?=?new?Task(action);Func<object,?string>?func?=?name?=>?"Hello?World"?+?name; Task<string>?task2?=?new?Task<string>(func,?"hystar"?,?CancellationToken.None,TaskCreationOptions.None?);//接收object參數真蛋疼,很不容易區分重載,把參數都寫上吧。執行這個Task對象需要手動調用Start方法:
task1.Start();這樣task對象將在默認的TaskScheduler調度下去執行,TaskScheduler使用線程池中的線程,至于是新建還是使用已有線程這個對用戶是完全透明的。還也可以通過重載函數的參數傳入自定義的TaskScheduler。
關于TaskScheduler的調度,推薦園子里這篇文章,前半部分介紹了一些線程執行機制,很值得一度。
當我們用new創建一個Task對象時,創建的對象是Created狀態,調用Start方法后將變為WaitingToRun狀態。至于什么時候開始執行(進入Running狀態,由TaskScheduler控制,)。Task的創建執行還有一種“快捷方式”,即Run方法:
Task.Run(()?=>?Console.WriteLine("Hello?World")); var?txt?=?await?Task<string>.Run(()?=>?"Hello?World");這種方式創建的Task會直接進入WaitingToRun狀態。
Task的其他狀態還有RanToCompletion,Canceled以及Faulted。在到達RanToCompletion狀態時就可以獲得Task<T>類型任務的結果。如果Task在狀態為Canceled的情況下結束,會拋出 OperationCanceledException。如果以Faulted狀態結束,會拋出導致任務失敗的異常。
Task同時服務于并發編程和異步編程(在Jeffrey?Richter的CLR via C#中分別稱這兩種模式為計算限制的異步操作和IO限制的異步操作,仔細想想這稱呼也很貼切),這里主要討論下Task和異步編程的相關的機制。其中最關鍵的一點就是Task是一個awaitable對象,這是其可以用于異步編程的基礎。除了Task,還有很多類型也是awaitable的,如ConfigureAwait方法返回的ConfiguredTaskAwaitable、WinRT平臺中的IAsyncInfo(這個后文有詳細說明)等。要成為一個awaitable類型需要符合哪些條件呢?其實就一點,其中有一個GetAwaiter()方法,該方法返回一個awaiter。那什么是awaiter對象呢?滿足如下3點條件即可:
-
實現INotifyCompletion或ICriticalNotifyCompletion接口
-
有bool類型的IsCompleted屬性
-
有一個GetResult()來返回結果,或是返回void
awaitable和awaiter的關系正如IEnumerable和IEnumerator的關系一樣。推而廣之,下面要介紹的async/await的幕后實現方式和處理yield語法糖的實現方式差不多。
Task類型的GetAwaiter()返回的awaiter是TaskAwaiter類型。這個TaskAwaiter很簡單基本上就是剛剛滿足上面介紹的awaiter的基本要求。類似于EAP,當異步操作執行完畢后,將通過OnCompleted參數設置的回調繼續向下執行,并可以由GetResult獲取執行結果。
?
簡要了解過Task,再來看一下本節的重點 - async異步方法。async/await模式的異步也出來很久了,相關文章一大片,這里介紹下重點介紹下一些不容易理解和值得重點關注的點。我相信我曾經碰到的困惑也是很多人的遇到的困惑,寫出來和大家共同探討。
語法糖
對async/await有了解的朋友都知道這兩個關鍵字最終會被編譯為.NET中和異步相關的狀態機的代碼。這一部分來具體看一下這些代碼,了解它們后我們可以更準確的去使用async/await,同時也能理解這種模式下異常和取消是怎樣完成的。
先來展示下用于分析反編譯代碼的例子,一個控制臺項目的代碼,這是能想到的展示異步方法最簡單的例子了,而且和實際項目中常用的代碼結構也差不太多:
//實體類 public?class?User {public?int?Id?{?get;?set;?}public?string?UserName?{?get;?set;?}?=?"hystar";public?string?Email?{?get;?set;?} }class?Program {static?void?Main(string[]?args){var?service?=?new?Service(new?Repository());var?name?=?service.GetUserName(1).Result;Console.WriteLine(name);} }public?class?Service {private?readonly?Repository?_repository;public?Service(Repository?repository){_repository?=?repository;}public?async?Task<string>?GetUserName(int?id){var?name?=?await?_repository.GetById(id);return?name;} }public?class?Repository {private?DbContext?_dbContext;private?DbSet<User>?_set;public?Repository(){_dbContext?=?new?DbContext("");_set?=?_dbContext.Set<User>();}public?async?Task<string>?GetById(int?id){?//IO...var?user?=?await?_set.FindAsync(id);return?user.UserName;} }注意:控制臺版本的示例代碼中在Main函數中使用了task.Result來獲取異步結果,需要注意這是一種阻塞模式,在除控制臺之外的UI環境不要使用類似Result屬性這樣會阻塞的方法,它們會導致UI線程死鎖。而對于沒有SynchronizationContext的控制臺應用確是再合適不過了。對于沒有返回值的Task,可以使用Wait()方法等待其完成。
這里使用ILSpy去查看反編譯后的代碼,而且注意要將ILSpy選項中的Decompile async methods (async/await)禁用(如下圖),否則ILSpy會很智能將IL反編譯為有async/await關鍵字的C#代碼。另外我也嘗試過Telerik JustDecompile等工具,但是能完整展示反編譯出的狀態機的只有ILSpy。
圖2
另外注意,應該選擇Release版本的代碼去查看,這是在一個Stackoverflow回答中看到的,說是有啥不同,具體也沒仔細看,這里知道選擇Release版exe/dll反編譯就好了。下面以Service類為例來看一下反編譯后的代碼:
圖3
通過圖上的注釋可以看到代碼主要由兩大部分構成,Service類原有的代碼和一個由編譯器生成的狀態機,下面分別具體了解下它們都做了什么。依然是以圖片加注釋為主,重要的部分會在圖后給出文字說明。
圖4
通過上圖中的注釋可以大致了解GetUserName方法編譯后的樣子。我們詳細介紹下其中幾個點,首先是AsyncTaskMethodBuilder<T>,我感覺很有必要列出其代碼一看:
為了篇幅關系,這里刪除了部分復雜的實現,取而代之的是介紹方法作用的注釋性文字,對于簡單的方法或是重要的方法保留了代碼。
namespace?System.Runtime.CompilerServices {public?struct?AsyncTaskMethodBuilder<TResult>{internal?static?readonly?Task<TResult>?s_defaultResultTask?=?AsyncTaskCache.CreateCacheableTask<TResult>(default(TResult));//這也是一個很重要的類,AsyncTaskMethodBuilder將一些操作進一步交給AsynchronousMethodBuilderCore來完成private?AsyncMethodBuilderCore?m_coreState;private?Task<TResult>?m_task;[__DynamicallyInvokable]public?Task<TResult>?Task{[__DynamicallyInvokable]get{Task<TResult>?task?=?this.m_task;if?(task?==?null){task?=?(this.m_task?=?new?Task<TResult>());}return?task;}}private?object?ObjectIdForDebugger{get{return?this.Task;}}[__DynamicallyInvokable]public?static?AsyncTaskMethodBuilder<TResult>?Create(){return?default(AsyncTaskMethodBuilder<TResult>);}//開始狀態機的執行[__DynamicallyInvokable,?DebuggerStepThrough,?SecuritySafeCritical]public?void?Start<TStateMachine>(ref?TStateMachine?stateMachine)?where?TStateMachine?:?IAsyncStateMachine{if?(stateMachine?==?null){throw?new?ArgumentNullException("stateMachine");}//保存當前ExecutionContext,這是很重要的一步,后文會具體介紹ExecutionContextSwitcher?executionContextSwitcher?=?default(ExecutionContextSwitcher);RuntimeHelpers.PrepareConstrainedRegions();try{ExecutionContext.EstablishCopyOnWriteScope(ref?executionContextSwitcher);stateMachine.MoveNext();}finally{executionContextSwitcher.Undo();}}[__DynamicallyInvokable]public?void?SetStateMachine(IAsyncStateMachine?stateMachine){this.m_coreState.SetStateMachine(stateMachine);}[__DynamicallyInvokable]public?void?AwaitOnCompleted<TAwaiter,?TStateMachine>(ref?TAwaiter?awaiter,?ref?TStateMachine?stateMachine)?where?TAwaiter?:?INotifyCompletion?where?TStateMachine?:?IAsyncStateMachine{try{AsyncMethodBuilderCore.MoveNextRunner?runner?=?null;Action?completionAction?=?this.m_coreState.GetCompletionAction(AsyncCausalityTracer.LoggingOn???this.Task?:?null,?ref?runner);if?(this.m_coreState.m_stateMachine?==?null){Task<TResult>?task?=?this.Task;this.m_coreState.PostBoxInitialization(stateMachine,?runner,?task);}awaiter.OnCompleted(completionAction);}catch?(Exception?arg_5C_0){AsyncMethodBuilderCore.ThrowAsync(arg_5C_0,?null);}}[__DynamicallyInvokable,?SecuritySafeCritical]public?void?AwaitUnsafeOnCompleted<TAwaiter,?TStateMachine>(ref?TAwaiter?awaiter,?ref?TStateMachine?stateMachine)?where?TAwaiter?:?ICriticalNotifyCompletion?where?TStateMachine?:?IAsyncStateMachine{try{AsyncMethodBuilderCore.MoveNextRunner?runner?=?null;//這是整個方法乃至類中最重要的一部分//獲取當前狀態執行完畢后下一步的操作Action?completionAction?=?this.m_coreState.GetCompletionAction(AsyncCausalityTracer.LoggingOn???this.Task?:?null,?ref?runner);if?(this.m_coreState.m_stateMachine?==?null){Task<TResult>?task?=?this.Task;this.m_coreState.PostBoxInitialization(stateMachine,?runner,?task);}//將下一步操作傳遞給awaiter對象,實際進入下一步還是通過awaiter來進行的。awaiter.UnsafeOnCompleted(completionAction);}catch?(Exception?arg_5C_0){AsyncMethodBuilderCore.ThrowAsync(arg_5C_0,?null);}}[__DynamicallyInvokable]public?void?SetResult(TResult?result){//設置結果//通過Task上的方法來完成}internal?void?SetResult(Task<TResult>?completedTask){//設置結果,調用上面的方法來完成????????????}public?void?SetException(Exception?exception){//設置異常//通過Task上的方法來實現}internal?void?SetNotificationForWaitCompletion(bool?enabled){this.Task.SetNotificationForWaitCompletion(enabled);}private?Task<TResult>?GetTaskForResult(TResult?result){//獲取Task包裝的結果}} }狀態機的幾種狀態如下:
-
-1:表示還未開始執行
-
-2:執行結束,可能是正常完成,也可能遇到異常處理異常后結束
-
0~:下一個狀態。如0表示初始的-1之后的下一個狀態,1表示0后的下一狀態,以此類推。
上面的類中還出現了一個很重要的類型AsyncMethodBuilderCore,簡單的了解一下這個類型也很有必要。
namespace?System.Runtime.CompilerServices {internal?struct?AsyncMethodBuilderCore{internal?sealed?class?MoveNextRunner{private?readonly?ExecutionContext?m_context;internal?IAsyncStateMachine?m_stateMachine;[SecurityCritical]private?static?ContextCallback?s_invokeMoveNext;[SecurityCritical]internal?MoveNextRunner(ExecutionContext?context,?IAsyncStateMachine?stateMachine){this.m_context?=?context;this.m_stateMachine?=?stateMachine;}[SecuritySafeCritical]internal?void?Run(){//這個方法被包裝為“繼續執行”委托實際執行的代碼//這個方法最終要的作用是給繼續執行的代碼設置正確的ExecutionContext}[SecurityCritical]private?static?void?InvokeMoveNext(object?stateMachine){((IAsyncStateMachine)stateMachine).MoveNext();}}private?class?ContinuationWrapper{internal?readonly?Action?m_continuation;private?readonly?Action?m_invokeAction;internal?readonly?Task?m_innerTask;internal?ContinuationWrapper(Action?continuation,?Action?invokeAction,?Task?innerTask){if?(innerTask?==?null){innerTask?=?AsyncMethodBuilderCore.TryGetContinuationTask(continuation);}this.m_continuation?=?continuation;this.m_innerTask?=?innerTask;this.m_invokeAction?=?invokeAction;}internal?void?Invoke(){this.m_invokeAction();}}internal?IAsyncStateMachine?m_stateMachine;internal?Action?m_defaultContextAction;public?void?SetStateMachine(IAsyncStateMachine?stateMachine){}//上文提到的獲取“繼續執行”委托的方法//方法通過包裝內部類MoveNextRunner的Run方法來實現[SecuritySafeCritical]internal?Action?GetCompletionAction(Task?taskForTracing,?ref?AsyncMethodBuilderCore.MoveNextRunner?runnerToInitialize){Debugger.NotifyOfCrossThreadDependency();ExecutionContext?executionContext?=?ExecutionContext.FastCapture();Action?action;AsyncMethodBuilderCore.MoveNextRunner?moveNextRunner;if?(executionContext?!=?null?&&?executionContext.IsPreAllocatedDefault){action?=?this.m_defaultContextAction;if?(action?!=?null){return?action;}moveNextRunner?=?new?AsyncMethodBuilderCore.MoveNextRunner(executionContext,?this.m_stateMachine);action?=?new?Action(moveNextRunner.Run);if?(taskForTracing?!=?null){action?=?(this.m_defaultContextAction?=?this.OutputAsyncCausalityEvents(taskForTracing,?action));}else{this.m_defaultContextAction?=?action;}}else{moveNextRunner?=?new?AsyncMethodBuilderCore.MoveNextRunner(executionContext,?this.m_stateMachine);action?=?new?Action(moveNextRunner.Run);if?(taskForTracing?!=?null){action?=?this.OutputAsyncCausalityEvents(taskForTracing,?action);}}if?(this.m_stateMachine?==?null){runnerToInitialize?=?moveNextRunner;}return?action;}private?Action?OutputAsyncCausalityEvents(Task?innerTask,?Action?continuation){}internal?void?PostBoxInitialization(IAsyncStateMachine?stateMachine,?AsyncMethodBuilderCore.MoveNextRunner?runner,?Task?builtTask){//初始化AsyncMethodBuilderCore中的狀態機變量。這里發生裝箱操作。}internal?static?void?ThrowAsync(Exception?exception,?SynchronizationContext?targetContext){//將異常與SynchronizationContext相關聯}internal?static?Action?CreateContinuationWrapper(Action?continuation,?Action?invokeAction,?Task?innerTask?=?null){return?new?Action(new?AsyncMethodBuilderCore.ContinuationWrapper(continuation,?invokeAction,?innerTask).Invoke);}internal?static?Action?TryGetStateMachineForDebugger(Action?action){//獲取用于調試目的的“繼續執行”委托}internal?static?Task?TryGetContinuationTask(Action?action){//獲取“繼續執行”的Task}} }總結來說AsyncTaskMethodBuilder<T>和AsyncMethodBuilderCore控制著狀態機的執行(主要是在正確的Context下調用MoveNext方法),并在執行狀態機的過程中負責正確的設置ExecutionContext和SynchronizationContext。
介紹了這么多基礎構造,你可能更關心原來的調用Repository的方法的代碼去哪了,它們在狀態機的代碼中。下面就來看一下狀態機:
圖5
通過注釋應該可以了解這個狀態機的細節了。
簡單的說一下這個struct優化。一開始狀態機被作為struct對象放置在棧上,對于await的工作已經完成不需要等待的情況,將快速結束狀態機,這樣狀態機直接出棧效率高。如果await的工作需要等待則控制異步方法執行的AsyncTaskMethodBuilder再將狀態機移動到堆中。因為這種情況下會發生Context切換(在SynchronizationContext不為空的情況下),如果狀態機還在棧上則會導致很大的切換負擔。
其實搞成一個狀態機的目的主要還是考慮到可能存在多個await的情況。對于只有1個await的情況其實狀態機的必要性不大,幾個if也就夠了,下面擴展下上面的例子看看有2個以上await(1個和2個await的狀態機都是使用if/else解決問題,從3個起開始不同)時編譯器產生的代碼,首先是擴展后的C#代碼(以WPF應用為例):
public?partial?class?MainWindow?:?Window {public?MainWindow(){InitializeComponent();}private?async?void?Button_Click(object?sender,?RoutedEventArgs?e){var?userService?=?new?Service();Debug.Write(Thread.CurrentThread.ManagedThreadId);var?avatar?=?await?userService.GetUserAvatarAsync(1);Debug.Write(Thread.CurrentThread.ManagedThreadId);//使用獲取的avatar} }public?class?Service {private?readonly?Repository?_repository;private?readonly?WebHepler?_webHelpler;private?readonly?ImageLib?_imgLib;public?Service(){_repository?=?new?Repository();_webHelpler?=?new?WebHepler();_imgLib?=?new?ImageLib();}public?async?Task<byte[]>?GetUserAvatarAsync(int?id){Debug.WriteLine("Service--"?+?Thread.CurrentThread.ManagedThreadId);var?user?=?await?_repository.GetByIdAsync(id);Debug.WriteLine("Service--"?+?Thread.CurrentThread.ManagedThreadId);var?email?=?user.Email;var?avatar?=?await?_webHelpler.GetAvatarByEmailAsync(email);Debug.WriteLine("Service--"?+?Thread.CurrentThread.ManagedThreadId);var?thumbnail?=?await?_imgLib.GetImgThumbnailAsync(avatar);return?thumbnail;} }public?class?Repository {private?readonly?DbContext?_dbContext;private?readonly?DbSet<User>?_set;public?Repository(){//_dbContext?=?new?DbContext("");//_set?=?_dbContext.Set<User>();}public?async?Task<User>?GetByIdAsync(int?id){Debug.WriteLine("Repo--"?+?Thread.CurrentThread.ManagedThreadId);//IO...var?user?=?await?_set.FindAsync(id);Debug.WriteLine("Repo--"?+?Thread.CurrentThread.ManagedThreadId);return?user;} }public?class?WebHepler {private?readonly?HttpClient?_httpClient;public?WebHepler(){_httpClient?=?new?HttpClient();}public?async?Task<byte[]>?GetAvatarByEmailAsync(string?email){Debug.WriteLine("Http--"?+?Thread.CurrentThread.ManagedThreadId);var?url?=?"http://avater-service-sample/"?+?email;var?resp?=?await?_httpClient.GetByteArrayAsync(url);Debug.WriteLine("Http--"?+?Thread.CurrentThread.ManagedThreadId);return?resp;} }public?class?ImageLib {public?async?Task<byte[]>?GetImgThumbnailAsync(byte[]?avatar){//模擬一個異步圖像處理任務return?await?Task.Run(()?=>{Task.Delay(500);return?avatar;});} }依然以Service類為例來分析await編譯后的樣子:
Service中的GetUserAvatar方法中的3個await將把函數體分割為4個異步區間,如下:
圖6
編譯生成的代碼最主要的不同是生成的狀態機變了,依舊是通過截圖和注釋來說一下這個新的狀態機的執行情況(方便對比,注釋將只標出與之前狀態機不同的部分):
圖7
通過上面的分析,async/await關鍵字背后的秘密已經清清楚楚。下面來說一下線程的問題。
?
線程!
關于async/await模式線程的問題,剛開始學習async/await那陣,看到很多文章,各種各樣的說法,一度讓我很迷惑。
一種觀點是很多國外同行的文章里說的:async/await本身不創建線程。StackoverFlow上很多回答也明確說async/await這兩個新增的關鍵字只是語法糖,編譯后的代碼不新建線程,這曾經一度給我造成了很大的困惑:“不創建線程的話要異步還有啥用!”。
后來看到一種觀點是園友jesse2013博文中的一句話:
await 不會開啟新的線程,當前線程會一直往下走直到遇到真正的Async方法(比如說HttpClient.GetStringAsync),這個方法的內部會用Task.Run或者Task.Factory.StartNew 去開啟線程。也就是如果方法不是.NET為我們提供的Async方法,我們需要自己創建Task,才會真正的去創建線程。
這個這個觀點應該是正確的,可后來看了很多代碼后感覺還不完全是這樣,畢竟一個被調用的async方法就會產生一個新的Task,而這個新的Task可能去“開啟一個新線程”。改造下上面的代碼測試這個問題:
public?class?Service {private?readonly?Repository?_repository;public?Service(Repository?repository){_repository?=?repository;}public?async?Task<string>?GetUserName(int?id){Console.WriteLine(Thread.CurrentThread.ManagedThreadId);var?name?=?await?_repository.GetById(id);Console.WriteLine(Thread.CurrentThread.ManagedThreadId);return?name;} }public?class?Repository {private?DbContext?_dbContext;private?DbSet<User>?_set;public?Repository(){_dbContext?=?new?DbContext("");_set?=?_dbContext.Set<User>();}public?async?Task<string>?GetById(int?id){????????//IO...var?user?=?await?_set.FindAsync(id);return?user.UserName;} }在控制臺應用中執行這段代碼會發現輸出的兩個線程Id是不相同的。
提示:控制臺引用程序沒有SynchronizationContext,在不恢復SynchronizationContext的情況下能更好的看出線程的變化。
到底情況是怎樣的呢,這里試著分析下我的想法:
這里先闡釋清“創建新線程”這個概念。我認為在這種情況下大家說的“創建新線程”可以被認為是與調用方法使用不同的線程,這個線程可能是線程池已有的,也可能是新建并被加入到線程池的線程。明確這給之后,繼續說線程問題。
首先肯定一點async/await關鍵字不會創建新線程是對的。如上文代碼中所示async/await被編譯為一個狀態機的確不參與Task的創建,實際新建Task的是被調用的異步方法。也就是說每調用一次異步方法(每一個await)都會產生一個新的Task,這個Task會自動執行。前面說過Task由TaskScheduler安排執行,一般都會在一個與調用線程不同的線程上執行。
為了把這個問題解釋清楚,假設調用異步方法的線程為A,異步方法啟動后在B線程執行。當B線程開始執行后,A線程將交出控制權。異步方法執行結束后,后續代碼(await后面的代碼)將在B線程上使用A線程的ExecutionContext(和SynchronizationContext,默認情況)繼續執行。
注意這個A線程到B線程控制權的轉換正是async異步模式的精髓之一。在WPF等這樣的客戶端環境這樣做不會阻塞UI線程,使界面不失去響應。在MVC這樣的Web環境可以及時釋放HTTP線程,使Web服務器可以接收更多請求。畢竟B線程這種線程池中的線程成本更低。這樣就是為什么既然也要花等待異步操作完成的時間,還要另外使用異步方法的原因 - 及時釋放調用線程,讓低成本的線程去處理耗時的任務。
最后當需要在發起執行的線程(這里是A線程)上繼續進行處理時只要獲得當時A線程的ExecutionContext和SynchronizationContext就可以了,并在這些Context完成剩余操作即可。
如果后續還有其他await,則會出現C線程,D線程等。如B調用了C的話,B的各種Context會被傳遞給C。當從異步方法返回后,執行的線程變了但是Context沒變。這樣異步方法給我們的感覺就像是同步一般。這也就是async/await方法的精妙之處。
那個Task的ConfigureAwait方法又是做什么用的呢,理解了上文就很好理解這個方法了。在異步方法返回時,會發生線程切換,默認情況下(ConfigureAwait(true)時)ExecutionContext和SynchronizationContext都會被傳遞。如果ConfigureAwait(false)則只有ExecutionContext會被傳遞,SynchronizationContext不會被傳遞。在WPF等客戶端程序UI部分,應該使用默認設置讓SynchronizationContext保持傳遞,這樣異步代碼的后續代碼才能正常操作UI。除此之外的其他情況,如上面的Service類中,都該使用ConfigureAwait(false)以放棄SynchronizationContext的傳遞來提高性能。
下面以圖應該會對上面這段文字有更深的了解:
吐槽一下,本來是想用vs生成的時序圖進行演示呢。結果發現vs2015取消這個功能了。手頭也沒有其他版本的vs。就用代碼截圖來掩飾這個線程變化過程吧。
首先是控制臺程序的線程變化情況:
圖8
因為控制臺應用沒有SynchronizationContext,所以可以清楚的看到線程的變化。
下面看看在WPF中類似流程執行的樣子:
圖9
可以看到在默認情況下每個await后的異步代碼返回到都回到UI線程,即所有await的后繼代碼都使用UI線程的SynchronizationContext來執行。除了調用方法外,其它所有的方法沒有必要返回UI線程,所以我們應該把除調用開始處(即Button_Click方法)外的所有異步調用都配置為ConfigureAwait(false)。
public?partial?class?MainWindow?:?Window {public?MainWindow(){InitializeComponent();}private?async?void?Button_Click(object?sender,?RoutedEventArgs?e){var?userService?=?new?Service();Debug.Write(Thread.CurrentThread.ManagedThreadId);var?avatar?=?await?userService.GetUserAvatarAsync(1);Debug.Write(Thread.CurrentThread.ManagedThreadId);//使用獲取的avatar} }public?class?Service {private?readonly?Repository?_repository;private?readonly?WebHepler?_webHelpler;public?Service(){_repository?=?new?Repository();_webHelpler?=?new?WebHepler();}public?async?Task<byte[]>?GetUserAvatarAsync(int?id){var?user?=?await?_repository.GetByIdAsync(id).ConfigureAwait(false);var?email?=?user.Email;var?avatar?=?await?_webHelpler.GetAvatarByEmailAsync(email).ConfigureAwait(false);return?avatar;} }public?class?Repository {private?readonly?DbContext?_dbContext;private?readonly?DbSet<User>?_set;public?Repository(){_dbContext?=?new?DbContext("");_set?=?_dbContext.Set<User>();}public?async?Task<User>?GetByIdAsync(int?id){//IO...var?user?=?await?_set.FindAsync(id).ConfigureAwait(false);return?user;} }public?class?WebHepler {private?readonly?HttpClient?_httpClient;public?WebHepler(){_httpClient?=?new?HttpClient();}public?async?Task<byte[]>?GetAvatarByEmailAsync(string?email){var?url?=?"http://avater-service-sample/"?+?email;var?resp?=?await?_httpClient.GetByteArrayAsync(url);return?resp;} }通過上面的圖,可以了解到有SynchronizationContext和沒有SynchronizationContext環境的不同,是否恢復SynchronizationContext的影響。對于ASP.NET環境雖然也有SynchronizationContext,但實測線程切換的表現比較詭異,實在無法具體分析,但按照WPF的方式來配置異步肯定是對的。
其它資料:據CLR via C#作者大神Jeffrey Richter在書中所說,.NET這種以狀態機實現異步的思想來自于其為.NET 4.0寫的Power Threading庫中的AsyncEnumerator類。可以將其作為一個參考來學習async異步方法的機制。
async異步編程中的取消和進度報告
由文章開始處的圖1可知,Task天生支持取消,通過一個接收CancellationToken的重載創建的Task可以被通知取消。
var?tokenSource?=?new?CancellationTokenSource(); CancellationToken?ct?=?tokenSource.Token; var?task?=?Task.Run(()?=>?Task.Delay(10000,ct),?ct); tokenSource.Cancel();自然我們異步方法的取消也離不開CancellationToken,方法就是給異步方法添加接收CancellationToken的重載,如前文示例代碼Service中的方法可以添加一個這樣的重載支持取消:
public?async?Task<byte[]>?GetUserAvatarAsync(int?id,?CancellationToken?ct) {... }async異步編程最大的一個特點就是傳播性,即如果有一個異步方法,則所有調用這個方法的方法都應該是異步方法,而不能有任何同步方法(控制臺應用Main函數中那種把異步轉同步的方式除外)。而通過CancellationToken實現的取消模式可以很好的適配這種傳播性,所需要做的就是把所有異步方法都添加支持CancellationToken的重載。之前的例子改造成支持取消后如下(展示一部分):
class?Program {static?void?Main(string[]?args){var?tokenSource?=?new?CancellationTokenSource();CancellationToken?ct?=?tokenSource.Token;var?userService?=?new?Service();var?avatar?=?userService.GetUserAvatarAsync(1,ct).Result;tokenSource.Cancel();Console.Read();} }public?class?Service {private?readonly?Repository?_repository;private?readonly?WebHepler?_webHelpler;public?Service(){_repository?=?new?Repository();_webHelpler?=?new?WebHepler();}public?async?Task<byte[]>?GetUserAvatarAsync(int?id,?CancellationToken?ct){var?user?=?await?_repository.GetByIdAsync(id,?ct);var?email?=?user.Email;ct.ThrowIfCancellationRequested();var?avatar?=?await?_webHelpler.GetAvatarByEmailAsync(email,?ct);return?avatar;} }注意ct.ThrowIfCancellationRequested()調用,這是可以及時取消后續未完成代碼的關鍵。當執行這個語句時,如果ct被標記取消,則這個語句拋出OperationCanceledException異常,后續代碼停止執行。
和取消機制一樣,新版的.NET也為進度通知提供了內置類型的支持。IProgress<T>和Progress<T>就是為此而生。類型中的泛型參數T表示Progress的ProgressChanged事件訂閱的處理函數的第二個參數的類型。擴展之前的例子,把它改成支持進度報告的方法:
class?Program {static?void?Main(string[]?args){var?progress?=?new?Progress<int>();progress.ProgressChanged?+=?(?s,?e?)?=>?{//e就是int類型的進度,可以使用各種方式進行展示。};var?userService?=?new?Service();var?avatar?=?userService.GetUserAvatarAsync(1,progress).Result;tokenSource.Cancel();Console.Read();} }public?class?Service {private?readonly?Repository?_repository;private?readonly?WebHepler?_webHelpler;public?Service(){_repository?=?new?Repository();_webHelpler?=?new?WebHepler();}public?async?Task<byte[]>?GetUserAvatarAsync(int?id,?IProgress<int>?progress){var?user?=?await?_repository.GetByIdAsync(id,?progress);//progress可以進一步傳遞,但注意進度值要在合理范圍內var?email?=?user.Email;progress.Report(50);//報告進度var?avatar?=?await?_webHelpler.GetAvatarByEmailAsync(email,?progress);progress.Report(100);return?avatar;} }可以看到在async異步模式下取消和進度都很容易使用。
?
以上介紹了擁有async/await支持的TAP異步編程。在編寫新的異步代碼時應該優先選用TAP模型,而且新版的.NET庫幾乎給所有同步接口增加了這種可以通過async/await使用的異步接口。但往往項目中會存在一些使用APM或EAP模式的代碼,通過下面介紹的一些方法可以使用async/await的方式調用這些代碼。
將BeginXXX/EndXXX的APM模式代碼轉為async異步方法只需要利用TaskFactory類的FromAsync方法即可,我們以介紹APM時提到的HttpWebRequest為例:
public?Task<WebResponse>?GetResponseAsync(WebRequest?client) {return?Task<WebResponse>.Factory.FromAsync(client.BeginGetResponse,?client.EndGetResponse,?null); }TaskFactory的FromAsync方法中使用TaskCompletionSource<T>來構造Task對象。
封裝EAP模式的代碼要比APM麻煩一些,我們需要手動構造TaskCompletionSource對象(代碼來自,手打的)。
WebClient?client; Uri?address; var?tcs?=?new?TaskCompletionSource<string>(); DownloadStringCompletedEventHandler?hander?=?null; handler?=?(_,?e)=> {client.DownloadStringCompleted?-=?handler;if(e.Cancelled)tcs.TrySetCanceled();else?if(e.Error?!=?null)tcs.TrySetException(e.Error);elsetcs.TrySetResult(e.Result); } client.DownloadStringCompleted?+=?handler; client.DownloadStringAsync(address);return?tcs.Task;可以看到TaskCompletionSource提供了一種手動指定Task結果來構造Task的方式。
?
上面寫了那么多,真沒有信息保證全部都是正確的。最后推薦3篇文章,相信它們對理解async異步方法會有很大幫助,本文的很多知識點也是來自這幾篇文章:
-
Understanding C# async / await (1) Compilation
-
Understanding C# async / await (2) Awaitable-Awaiter Pattern
-
Understanding C# async / await (3) Runtime Context
?
WinRT 異步編程 C#
WinRT是完全不同于.NET的一種框架,目地就是把Windows的底層包裝成API讓各種語言都可以簡單的調用。WinRT中對異步的實現也和.NET完全不同,這一小節先看一下WinRT中異步機制的實現方法,再來看一下怎樣使用C#和.NET與WinRT中的異步API進行交互。
前文提到async異步編程中兩個比較重要的對象是awaitable和awaiter。在WinRT中充當awaitable的是IAsyncInfo接口的對象,具體使用中有如下4個實現IAsyncInfo接口的類型:
-
IAsyncAction
-
IAsyncActionWithProgress<TProgress>
-
IAsyncOperation<TResult>
-
IAsyncOperationWithProgress<TResult, TProgress>
由泛型參數可以看出Action和Operation結尾的兩個類型不同之處在于IAsyncAction的GetResults方法返回void,而IAsyncOperation<TResult>的GetResults方法返回一個對象。WithProgress結尾的類型在類似類型的基礎上增加了進度報告功能(它們內部定義了Progress事件用來執行進度變更時的處理函數)。
Task和IAsyncInfo分別是對.NET和WinRT中異步任務的包裝。它們的原理相同但具體實現有所不同。IAsyncInfo表示的任務的狀態(可以通過Status屬性查詢)有如下幾種(和Task對照,整理自MSDN):
| Task狀態 (TaskStatus類型) | IAsyncInfo狀態 (AsyncStatus類型) |
| RanToCompletion | Completed |
| Faulted | Error |
| Canceled | Canceled |
| 所有其他值和已請求的取消 | Canceled |
| 所有其他值和未請求的取消 | Started |
另外獲取異常的方式也不一樣,通過Task中的Exception屬性可以直接得到.NET異常,而IAsynInfo中錯誤是通過ErrorCode屬性公開的一個HResult類型的錯誤碼。當時用下文價紹的方法將IAsynInfo轉為Task時,HResult會被映射為.NET Exception。
之前我們說這些IAsyncXXX類型是awaitable的,但為什么這些類型中沒有GetAwaiter方法呢。真相是GetAwaiter被作為定義在.NET的程序集System.Runtime.WindowsRuntime.dll中的擴展方法,因為基本上來說async/awati還是C#使用的關鍵字,而C#主要以.NET為主。
這些擴展方法聲明形如(有多個重載,下面是其中2個):
public?static?TaskAwaiter?GetAwaiter<TResult>(this?IAsyncAction?source); public?static?TaskAwaiter<TResult>?GetAwaiter<TResult,?TProgress>(this?IAsyncOperationWithProgress<TResult,?TProgress>?source);我們又見到了熟悉的TaskAwaiter。這個方法的實現其實也很簡單(以第一個重載為例):
public?static?TaskAwaiter?GetAwaiter(this?IAsyncAction?source) {return?WindowsRuntimeSystemExtensions.AsTask(source).GetAwaiter(); }可以看到就是通過task.GetAwaiter得到的TaskAwaiter對象。
這一系列擴展方法的背后又有一個更重要的擴展方法 - AsTask()。
AsTask方法有更多的重載,其實現原理和前文介紹將EAP包裝為async異步模式的代碼差不多,都是通過TaskCompletionSource來手工構造Task。下面展示的是一個最復雜的重載的實現:
public?static?Task<TResult>?AsTask<TResult,?TProgress>(this?IAsyncOperationWithProgress<TResult,?TProgress>?source,?CancellationToken?cancellationToken,?IProgress<TProgress>?progress) {if?(source?==?null)throw?new?ArgumentNullException("source");TaskToAsyncOperationWithProgressAdapter<TResult,?TProgress>?withProgressAdapter?=?source?as?TaskToAsyncOperationWithProgressAdapter<TResult,?TProgress>;if?(withProgressAdapter?!=?null?&&?!withProgressAdapter.CompletedSynchronously){Task<TResult>?task?=?withProgressAdapter.Task?as?Task<TResult>;if?(!task.IsCompleted){if?(cancellationToken.CanBeCanceled?&&?withProgressAdapter.CancelTokenSource?!=?null)WindowsRuntimeSystemExtensions.ConcatenateCancelTokens(cancellationToken,?withProgressAdapter.CancelTokenSource,?(Task)?task);if?(progress?!=?null)WindowsRuntimeSystemExtensions.ConcatenateProgress<TResult,?TProgress>(source,?progress);}return?task;}switch?(source.Status){case?AsyncStatus.Completed:return?Task.FromResult<TResult>(source.GetResults());case?AsyncStatus.Canceled:return?Task.FromCancellation<TResult>(cancellationToken.IsCancellationRequested???cancellationToken?:?new?CancellationToken(true));case?AsyncStatus.Error:return?Task.FromException<TResult>(RestrictedErrorInfoHelper.AttachRestrictedErrorInfo(source.get_ErrorCode()));default:if?(progress?!=?null)WindowsRuntimeSystemExtensions.ConcatenateProgress<TResult,?TProgress>(source,?progress);AsyncInfoToTaskBridge<TResult,?TProgress>?infoToTaskBridge?=?new?AsyncInfoToTaskBridge<TResult,?TProgress>();try{source.Completed?=?new?AsyncOperationWithProgressCompletedHandler<TResult,?TProgress>(infoToTaskBridge.CompleteFromAsyncOperationWithProgress);infoToTaskBridge.RegisterForCancellation((IAsyncInfo)?source,?cancellationToken);}catch{if?(Task.s_asyncDebuggingEnabled)Task.RemoveFromActiveTasks(infoToTaskBridge.Task.Id);throw;}return?infoToTaskBridge.Task;} }通過參數可以看到,這個轉換Task的過程支持調用方法傳入的取消和進度報告。如果我們需要調用的WinRT異步方法的過程中支持取消和進度報告,就不能直接await那個異步方法(相當于調用了默認無參的AsTask的返回task上的GetAwaiter方法),而是應該await顯示調用的AsTask(可以傳入CancellationToken及IProgress參數的重載,上面那個)返回的task對象。這個可以見本小節末尾處的例子。
回頭看一下上面給出的AsTask的實現。里面一個最終要的對象就是TaskToAsyncOperationWithProgressAdapter<TResult, TProgress>,其可以由IAsyncOperationWithProgress<TResult, TProgress>直接轉型而來。它也是IAsyncOperationWithProgress<TResult, TProgress>和Task之間的一個橋梁。這個類的工作主要由其父類TaskToAsyncInfoAdapter<TCompletedHandler, TProgressHandler, TResult, TProgressInfo>來完成。這個父類的實現就比較復雜了,但道理都是相同的。有興趣的同學自行查看其實現吧。
?
了解了原理最后來看一下代碼示例,WinRT中所有的IO相關的類中只提供異步方法,示例因此也選擇了這個使用最廣泛的功能(示例代碼來源是某開源庫,具體是啥忘了,有輕微改動):
public?async?Task<string>?ReadTextAsync(string?filePath) {var?text?=?string.Empty;using?(var?stream?=?await?ReadFileAsync(filePath)){using?(var?reader?=?new?StreamReader(stream)){text?=?await?reader.ReadToEndAsyncThread();}}return?text; }有了async/await和上文介紹的擴展方法的支持,C#調用WinRT的異步接口和使用.NET中的異步接口一樣的簡單。
如果是需要傳遞取消和進度報告怎么辦呢?
public?async?Task<string>?ReadTextAsync(string?filePath,?CancellationToken?ct,?IProgress<int>?progress) {var?text?=?string.Empty;try{using?(var?stream?=?await?ReadFileAsync(filePath).AsTask(ct,?progress)){using?(var?reader?=?new?StreamReader(stream)){text?=?await?reader.ReadToEndAsyncThread().AsTask(ct,?progress);}}}catch(OperationCanceledException)?{...}return?text; }代碼的簡潔程度讓你感到震撼吧。而且得到Task對象后,不但可以方便的配置取消和進度報告,還能通過ConfigureAwait來配置SynchronizationContext的恢復。
不知道參數ct和progress怎么來的同學可以看上一小節的取消和異步部分。
除了由IAsyncInfo到Task的轉換外,還可以由Task/Task<T>轉為IAsyncAction/IAsyncOperation<T>。這個轉換的主要作用是把C#寫的代碼封裝為WinRT供其它語言調用。實現這個操作的AsAsyncAction/AsAsyncOperation<T>方法也是定義于上面提到的System.Runtime.WindowsRuntime.dll程序集中。以本文第一小節的Service類為例,將其GetUserName方法改造成返回IAsyncOperation<string>的方法,如下:
public?class?Service {private?readonly?Repository?_repository;public?Service(Repository?repository){_repository?=?repository;}public?IAsyncOperation<string>?GetUserName(int?id){var?nameAsync?=?_repository.GetByIdAsync(id).AsAsyncOperation();return?nameAsync;} }這兩個擴展方法是用簡單方便,但有一點不足的就是不能支持Task中的取消和進度報告。要解決這個問題可以使用IAsyncInfo的Run方法來獲得IAsynInfo對象。Run方法支持多種不同類型的委托對象作為參數,比較復雜的一種可以支持取消和進度報告作為委托對象(一般是lambda表達式)的參數,比如把上面的例子改成支持取消和進度報告后如下:
public?class?Service {private?readonly?Repository?_repository;public?Service(Repository?repository){_repository?=?repository;}private?async?Task<string>?GetUserNameInternal(int?id,?){var?name?=?await?_repository.GetByIdAsync(id,?ct,?progress);return?name;}public?IAsyncOperation<string>?GetUserName(int?id,?CancellationToken?ct,?IProgress<int>?progress){var?nameAsync?=?AsyncInfo.Run(async?(ct,?progress)=>{?var?name?=?await?GetUserNameInternal(id,?ct,?progress);return?name;};return?nameAsync;} }內幕這樣就輕松的實現了將C#編寫的代碼作為WinRT組件的過程。從如下AsAsyncOperation和AsyncInfo.Run的反編譯代碼來看,很難知道這個方法的實現細節,畢竟它們都是和WinRT Native代碼相關的部分。
public?static?IAsyncOperation<TResult>?AsAsyncOperation<TResult>(this?Task<TResult>?source) {return?(IAsyncOperation<TResult>)?null; }public?static?IAsyncAction?Run(Func<CancellationToken,?Task>?taskProvider) {return?(IAsyncAction)?null; }?
WinRT異步編程 C++
微軟對C++進行了擴展,一方面是為C++實現類似C#中基于Task的線程管理方式,另一方面讓C++(準確說是C++/CX)可以實現與WinRT規范的的異步接口互操作。
這些擴展主要定義于ppltask.h中,concurrency命名空間下。
concurrency::task
先來看一下和.NET Task基本等價的task類型。這也是微軟C++擴展中并發異步線程管理的核心類型之一。微軟圍繞concurrency::task的設計的一些方法與C#中的Task相關方法真的非常下。下面的表格對比了C#的Task與C++中的concurrency::task。有C# Task基礎的話,對于concurrency::task很容易就能上手。
?
| ? | C# Task | C++ concurrency::task |
| 構造 方式1 | constructor | constructor |
| 構造 方式2 | Task.Factory.StartNew() | 用于異步 - create_task() |
| 構造 方式3 | 用于并行 -?make_task() 返回task_handle,和task_group等同用。 | |
| 阻塞 - 等待完成 | task.Wait() | task::wait() |
| 阻塞 - 等待獲取結果 | GetAwaiter().GetResult() | task::get() |
| 任務狀態類型 | TaskStatus | concurrency::task_status |
| 并行 - 等待全部 | Task.WhenAll() | concurrency::when_all |
| 并行 - 等待部分 | Task.WhenAny() | concurrency::when_any |
| 異步 - 任務延續 | Task.ContinueWith() | task::then() |
?
接著討論一下本節的重點內容,微軟給C++帶來的異步支持。
普通異步
看過之前介紹C#異步的部分,可以知道支持異步的系統無非就由以下以下幾部分組成:任務創建、任務延續、任務等待、取消、進度報告等。依次來看一下ppltask.h中支持這些部分的方法。
create_task方法可以將函數對象(廣義上的函數對象包含如lambda表達式,在C++11中也多用lambda表達式作為函數對象)包裝成task類對象。如上文所述,定義在ppltask.h中,位于concurrency命名空間下的task類和異步方法關系最密切。下面的代碼示例了concurrency::task的創建。
?task<int>?op1?=?create_task([]()??
{??
?????return?0;??
});
在C++11中一般都使用auto直接表示一些復雜的類型,讓編譯器去推斷。例子中寫出完整的類型可以讓讀者更好的理解方法的返回類型。
而類似于.NET Task中的ContinueWith方法的task::then方法,基本使用如下:
?op1.then([](int?v){??
?????return?0;??
});
在C++中由于沒有類似C#中async/await關鍵字的支持,所以后續任務不能像C#中那樣直接跟在await ...語句后,必須通過task::then方法來設置。
then方法也可以實現鏈式調用,如:
?auto?t?=?create_task([]()??
{??
?????//do?something??
}).then([](int?v){??
?????return?0;??
});
關于后續代碼執行上下文的問題,如果create_task方法接受的函數對象返回的是task<T>或task<void>則后續代碼會在相同的線程上下文運行,如果返回的是T或void則后續任務會在任意上下文運行。可以使用concurrency::task_continuation_context來更改這個設置。具體用法是將task_continuation_context傳給task::then其中那些接受task_continuation_context類型參數的重載。如果參數值為concurrency::task_continuation_context::use_arbitrary,則表示指定延續在后臺線程上運行,如果參數值為concurrency::task_continuation_context::use_current,則表示指定延續在調用了task::then的線程上運行。如:
?
?auto?t?=?create_task([]()??
{??
?????//do?something??
}).then([](int?v){??
?????//do?something?else;??
},task_continuation_context::use_arbitrary());//then()中傳入的代碼將在后臺線程執行,相對于C#中配置ConfigAwait(false)。
對于取消和異步的支持,將在下一小段進行介紹,那里的實現方式同樣可以應用到這一部分中。
使用create_task的方式創建task的方法只用于C++內部對task的管理。如果是希望將異步作為WinRT組件發布需要使用下面介紹的create_async。
如果是純C++中處理多線程任務,除了使用Windows中所提供的task,還可以考慮C++11標準庫中的thread,后者跨平臺更好。后文會有一部分介紹C++11的thread。如果是對C#的TPL模型很熟悉,轉到C++使用ppltask.h中的task會發現模型一致性很高。
?
支持WinRT的異步
1. 提供WinRT標準的異步方法
通過create_async方法可以將函數轉為異步函數,即這個方法是返回IAsyncInfo對象的。通過這個方法可以將代碼包裝成WinRT中標準的異步方法供其它語言調用。被包裝的代碼一般是可調用對象,在C++11中一般都使用Lambda表達式。返回的IAsyncInfo的具體類型(上文介紹的四種之一)是有傳入的參數決定的。
create_async的聲明:
?template<typename?_Function>
__declspec(
???noinline
)?auto?create_async(const?_Function&?_Func)?->?decltype(ref?new?details::_AsyncTaskGeneratorThunk<_Function>(_Func));
可以看到為了確定這個模板方法的返回類型使用了C++11的decltype和位置返回類型等新特性。
通常情況下,傳入create_async的函數對象的方法體是一般的代碼。還以把create_task方法的調用傳入create_async接收的lambda表達式的方法體中,create_task返回的concurrency::task也可以配置一系列的then(),最終這些配置都將反應給最外部的create_async的包裝。
下面的代碼就是包裝了最簡單的過程代碼:
?IAsyncOperation<int>^?op2?=?create_async([]()??
{??
?????return?0;??
});
也可以像上面說的包裝一段create_task的代碼(把C++內部的任務暴露給WinRT接口):
?IAsyncOperation<int>^?op3?=?create_async([](){
????return?create_task(KnownFolders::DocumentsLibrary->GetFileAsync("Dictionary.txt")).then([](StorageFile^?file)
????{????????
????????int?wordNum?=?0;
????????//?獲取單詞數
????????return?wordNum;
????};
});
通過create_async的重載也可以輕松的支持取消和進度報告。
擴展的C++使用的異步模式與C# TPL使用的標記式取消模型一致,但在使用上還是稍有不同,在介紹這種模式之前,先來說說取消延續的問題,如下面的代碼:
?auto?t1?=?create_task([]()?->?int
{
????//取消任務
????cancel_current_task();
});
auto?t2?=?t1.then([](task<int>?t)
{
????try
????{
????????int?n?=?t.get();
????????wcout?<<?L"后續任務"?<<?endl;
????}
????catch?(const?task_canceled&?e)
????{
????????
????}
});
auto?t3?=?t1.then([](int?n)
{
????wcout?<<?L"后續任務"?<<?endl;
});
這個例子中可以看到,我們可以在task內部方法中通過cancel_current_task()調用來取消當前的任務。如果t1被手動取消,對于t1的兩個后繼任務t2和t3,t2會被取消,t3不會被取消。這是由于t2是基于值延續的延續,而t3是基于任務的延續。
接下來的示例展示了C++中 的標記式取消:
?cancellation_token_source?cts;
auto?token?=?cts.get_token();
auto?t?=?create_task([]
{
????bool?moreToDo?=?true;
????while?(moreToDo)
????{
//是不是的檢查是否取消被設置
????????if?(is_task_cancellation_requested())
????????{
//取消任務
????????????cancel_current_task();
????????}
????????else?
????????{
????????????moreToDo?=?do_work();
????????}
????}
},?token).then([]{
//?延續任務
},token,concurrency::task_continuation_context::use_current);//傳遞取消標記,接收取消標記的重載還需要延續上下文的參數
//?觸發取消
cts.cancel();
t.wait();
通過使用cancellation_token,取消也可以傳遞到基于任務的延續。
上面演示的例子cancellation_token是在create_async方法內部定義的,更常見的情況在create_async的工作方法參數中顯示聲明cancellation_token并傳入到工作方法內,這樣IAsyncXXX上面的Cancel方法被調用,取消標志也會被自動設置,從而觸發鏈式的標記性取消。
說起來很抽象,可以參考下面的代碼:
?IAsyncAction^?DoSomething(){
return?create_async([](cancellation_token?ct)
{
auto?t?=?create_task([ct]()
{
//?do?something
});
});
}
這樣當DoSomething返回值(IAsyncAction對象)的Cancel方法被調用后,ct被標記為取消,任務t會在合適的時間被取消執行。
C++的cancellation_token有一個更高級的功能:其上可以設置回調函數,當cts觸發取消時,token被標記為取消時,會執行這個回調函數的代碼。
?cancellation_token_registration?cookie;
cookie?=?token.register_callback([&e,?token,?&cookie]()
{
????//?記錄task被取消的日志等
????//?還可以取消注冊的回調
????token.deregister_callback(cookie);
});
說完取消,再來看一下進度報告。下面的例子基本是演示進度報告最簡單的例子。
?IAsyncOperationWithProgress<int,?double>^?DoSometingWithProgressAsync(int?input)
{
????return?create_async([this,?input](progress_reporter<double>?reporter)?->?int
????{
auto?results?=?input;
reporter.report(1);
//?do?something
reporter.report(50);
//?do?something
reporter.report(100.0);
????????return?results;
????});
}
我們將一個concurrency::progress_reporter<T>對象當作參數傳入create_async接收的工作函數。然后就可以使用reporter的report方法來報告進度。返回的IAsyncOperationWithProgress類型可以使這個進度報告與WinRT中調用這個方法的代碼協同工作。
?
2. 調用WinRT標準的異步方法
說了創建異步方法,再來看看使用C++調用WinRT的異步方法。由于C++中沒有async/await那樣的異步模式,所以最值得關心的就是如何,所以當一個任務完成后需要手動傳入剩余的代碼來繼續后續任務的執行,這里需要用到task的then方法,首先我們需要把IAsyncInfo轉為task。(其實上面的代碼已經演示了這個用法)
不同于C#中通過AsTask方法將IAsyncInfo等類型轉為Task對象。C++中是使用create_task的方法(就是上面介紹的那個,不同的重載)來完成這個工作:
auto?createFileTadk?=create_task(folder->CreateFileAsync("aa.txt",CreationCollisionOption::ReplaceExisting));接著調用task的then方法設置后續執行:
?createFileTadk.then([this](StorageFile^?storageFileSample)?{??
?????????String^?filename=storageFileSample->Name;
????????});
?
捕獲異常方面,不涉及WinRT的部分遵循C++的異常捕獲原則,WinRT交互部分,需要保證拋出的異常可以被WinRT識別處理。
除了使用ppltask.h中的擴展,還可以使用WRL中的AsyncBase模板類來實現C++對WiinRT異步的支持。但后者的代碼過于晦澀,就不再介紹了。
說回來和WinRT交互就好用的語言還是C#,C++可以用于實現純算法部分,即位于WinRT下方的部分,只需要在必要的時候通過WinRT公開讓C#可調用的接口。這樣代碼的編寫效率和執行效率都很高。另外C#的應用商店程序支持本地編譯也是大勢所趨,在WinRT之上使用C#或C++/CX區別不大。
?
C++ 11 線程&并發&異步
C++在沉寂多年之后,終于在新版標準中迎來爆發,其中標準內置的線程支持就是一個完全全新的特性。在之前版本的C++中沒有標準的線程庫,實現跨平臺的線程操作一般都要借助于第三方的庫。現在有了C++11,相同的操作線程的代碼可以在不同的編譯器上編譯執行從而可以實現跨平臺的線程操作。
C++新標準中的線程,異步等看起來和C#的機制非常的像,不知道微軟和C++標準委員會誰“借鑒”的誰。
下面按線程,并發中同步支持,異步這樣的順序來逐個了解下C++新標準中增加的這些特性。介紹方式以C#的等價機制做對比,篇幅原因很多都是一個綱領作用,介紹一筆帶過,根據需要大家自行查找相應的功能的具體使用方法。
線程
C++11標準庫中引入了std::thread作為抽象線程的類型。其很多操作和.NET中的Thread類似。
| ? | C++ 11 | C# |
| ? | std::thread | Thread |
| 創建 | constructor | constructor |
| 插入一個線程 | t.join() ?t表示std::thread對象,下同 | t.Join() t表示Thread對象,下同 |
| 分離線程 | t.detach() | 無 |
| 獲取線程id | t.get_id() | Thread.CurrentThread.ManagedThreadId |
| 線程休眠 | std::this_thread::sleep_for() | Thread.Sleep() |
一段簡單的綜合示例代碼:
int?main() {std::thread?t1([](int?a){?std::this_thread::sleep_for(std::chrono::seconds(2))?},?3);t1.join();t1.detach();return?0;???? }?
多線程 - 互斥
C++11中內建了互斥機制,可以讓多個線程安全的訪問同一個變量。幾種機制總結如下(可能并非完全一直,但效果上很類似)
| ? | C++ 11 | C# |
| 原子類型 | atomic_type std::atomic<T> | Interlocked |
| 內存柵欄 | memory_order_type? | MemoryBarrier |
| 線程本地存儲 | thread_local | ThreadStatic LocalDataStoreSlot ThreadLocal<T> |
| 互斥 | std::mutex std::timed_mutex std::recursive_mutex std::recursive_timed_mutex | Mutex |
| 鎖 | lock_guard<T> | lock |
| 通知 | condition_variable condition_variable_any (notify_one/notify_all) | ManualResetEvent AutoResetEvent |
| 初始化 | call_once | ? |
上面介紹的線程或多線程支持都是一些很底層的接口。針對異步操作C++11還提供了一些高級接口,其中具有代表性的對象就是std::future和std::async。
std::future和C#中的TaskAwaiter比較相似,而std::async作用正如C#中使用async關鍵字標記的異步方法。在C++11中通過std::async將一個可調用對象包裝廠一個異步方法,這個方法將返回一個std::future對象,通過std::future可以得到異步方法的結果。
看一下這段代碼(來自qicosmos老師的博文)就能明白上面所說:
?std::future<int>?f1?=?std::async(std::launch::async,?[](){?
????????return?8;??
????});?
cout<<f1.get()<<endl;
關于C++11異步方面的特性,強烈推薦qicosmos老師的博文以及他編寫的圖書《深入應用C++11:代碼優化與工程級應用》。
?
C# 方法調用方信息
新版本的C#提供了方便獲取方法調用者信息的功能,對于需要調試以及輸出一些日志的情況很有用。這樣我們不需要像之前那樣在每個需要記錄日志的地方硬編碼下調用的方法名,提高了代碼的可讀性。
提供這個新功能的是幾個應用于參數的Attribute:
-
CallerFilePathAttribute 獲得調用方法所在的源文件地址
-
CallerLineNumberAttribute 被調用代碼的行號
-
CallerMemberNameAttribute 調用方法的名稱
使用其簡單只需要聲明一個參數,然后把這些Attribute加在參數前面,在函數中取到的參數值就是我們想要的結果。一個簡單的例子如下:
static?void?Caller() {Called(); }static?void?Called([CallerMemberName]?string?memberName?=?"",[CallerFilePath]?string?sourceFilePath?=?"",[CallerLineNumber]?int?sourceLineNumber?=?0) {Console.WriteLine(memberName);Console.WriteLine(sourceFilePath);Console.WriteLine(sourceLineNumber); }輸出如下:
Main
C:\Users\...\ConsoleApplication1\Program.cs
31
還算是簡單方便,尤其對于輸出日志來說。
?
C#5.0還對Lambda捕獲閉包外變量進行了一些小優化,這個在之前文章介紹Lambda時有介紹,這里不再贅述。
?
C++ 調用方法信息
在C中就有宏來完成類似的功能。由于C++可以兼容C,所以在C++11之前,一般都用這種C兼容的方式來獲得被調用方法的信息。新版的C++對此進行了標準化,增加了一個名為__func__的宏來完成這個功能。
需要注意的是和C#中類似功能獲得調用方法名稱不同,這個__func__宏得到的是被調用方法,即__func__所在方法的名稱。個人感覺C++中__func__更實用。仍然是一個簡單的例子:
?void?Called()
{
????std::cout?<<?__func__?<<?std::endl;
}
void?Caller()
{
????Called();
}
調用Caller()將輸出"Called"。
C++中實現這個宏的方式就是在編譯過程中在每個方法體的最前面插入如下代碼:
static?const?char*?__func__?=?"Called";了解這個之后你會感覺這個宏沒有那么神秘了。
除了新被標準化的__func__在大部分C++編譯器中仍然可以使用__LINE__和__FILE__獲取當前行號和所在文件。
?
預告
下篇文章將介紹C#6帶來的新特性,C#6中沒有什么重量級的改進(據說編譯器好像有很大改動,那個不了解就不說了,不是一般用戶能仔細研究的。編譯前端和編譯后端發展這么多年復雜程度接近操作系統了),大都是一些語法糖,而且糖的數量還不少。歡迎繼續關注。
?
本文斷斷續續寫了很久,中間還出去玩了2周。有什么錯誤請指正。
轉載于:https://www.cnblogs.com/lsxqw2004/p/4922374.html
總結
以上是生活随笔為你收集整理的【转】C#与C++的发展历程第一 - 由C#3.0起的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【转】微软Azure Functions
- 下一篇: 特斯拉裁员“屠龙刀”终于落下!马斯克:感