系列文章目錄html
2. C#與C++的發展歷程第二 - C#4.0再接再礪web
3. C#與C++的發展歷程第三 - C#5.0異步編程的巔峯算法
C#5.0做爲第五個C#的重要版本,將異步編程的易用度推向一個新的高峯。經過新增的async和await關鍵字,幾乎可使用編寫同步代碼的方式來編寫異步代碼。編程
本文將重點介紹下新版C#的異步特性以及部分其餘方面的改進。同時也將介紹WinRT程序一些異步編程的內容。c#
寫async異步編程這部份內容以前看了好多文章,反覆整理本身的思路,盡力保證文章的正確性。儘管如此仍然可能存在錯誤,請廣大園友及時指出,感謝感謝。後端
異步編程不是一個新鮮的話題,最先期的C#版本也內建對異步編程的支持,固然在顏值上沒法與目前基於TAP,使用async/await的異步編程相比。異步編程要解決的問題就是許多耗時的IO可能會阻塞線程致使CPU空轉下降效率,或者一個長時間的後臺任務會阻塞用戶界面。經過將耗時任務異步執行來使系統有更高的吞吐量,或保持界面的響應能力。如界面在加載一幅來自網絡的圖像時,還運行用戶進行其餘操做。安全
按前文慣例先上一張圖通覽一下TAP模式下異步編程的方方面面,而後由異步編程的發展來討論一下TAP異步模式。服務器
圖1cookie
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兩個最大的缺點是不支持進度報告以及不能方便的「取消」。
EAP
在C# .NET第二個版本中,增長了一種新的異步編程模型EAP(Event-based Asynchronous Pattern),EAP模式的異步代碼中,典型特徵是一個Async結尾的方法和Completed結尾的事件。XXXCompleted事件將在異步處理完成時被觸發,在事件的處理函數中能夠操做異步方法的結果。每每在EAP代碼中還會存在名爲CancelAsync的方法用來取消異步操做,以及一個ProgressChenged結尾的事件用來彙報操做進度。經過這種方式支持取消和進度彙報也是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); else tcs.TrySetResult(e.Result); } client.DownloadStringCompleted += handler; client.DownloadStringAsync(address); return tcs.Task;
能夠看到TaskCompletionSource提供了一種手動指定Task結果來構造Task的方式。
上面寫了那麼多,真沒有信息保證所有都是正確的。最後推薦3篇文章,相信它們對理解async異步方法會有很大幫助,本文的不少知識點也是來自這幾篇文章:
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; }
微軟對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++在沉寂多年以後,終於在新版標準中迎來爆發,其中標準內置的線程支持就是一個徹底全新的特性。在以前版本的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#提供了方便獲取方法調用者信息的功能,對於須要調試以及輸出一些日誌的狀況頗有用。這樣咱們不須要像以前那樣在每一個須要記錄日誌的地方硬編碼下調用的方法名,提升了代碼的可讀性。
提供這個新功能的是幾個應用於參數的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++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周。有什麼錯誤請指正。