C#與C++的發(fā)展歷程第三 - C#5.0異步編程巔峰
系列文章目錄
1. C#與C++的發(fā)展歷程第一 - 由C#3.0起
2. C#與C++的發(fā)展歷程第二 - C#4.0再接再厲
3. C#與C++的發(fā)展歷程第三 - C#5.0異步編程的巔峰
C#5.0作為第五個C#的重要版本,將異步編程的易用度推向一個新的高峰。通過新增的async和await關鍵字,幾乎可以使用編寫同步代碼的方式來編寫異步代碼。
本文將重點介紹下新版C#的異步特性以及部分其他方面的改進。同時也將介紹WinRT程序一些異步編程的內(nèi)容。
C# async/await異步編程
寫async異步編程這部分內(nèi)容之前看了好多文章,反復整理自己的思路,盡力保證文章的正確性。盡管如此仍然可能存在錯誤,請廣大園友及時指出,感謝感謝。
異步編程不是一個新鮮的話題,最早期的C#版本也內(nèi)建對異步編程的支持,當然在顏值上無法與目前基于TAP,使用async/await的異步編程相比。異步編程要解決的問題就是許多耗時的IO可能會阻塞線程導致CPU空轉(zhuǎn)降低效率,或者一個長時間的后臺任務會阻塞用戶界面。通過將耗時任務異步執(zhí)行來使系統(tǒng)有更高的吞吐量,或保持界面的響應能力。如界面在加載一幅來自網(wǎng)絡的圖像時,還運行用戶進行其他操作。
按前文慣例先上一張圖通覽一下TAP模式下異步編程的方方面面,然后由異步編程的發(fā)展來討論一下TAP異步模式。

圖1
APM
C# .NET最早出現(xiàn)的異步編程模式被稱為APM(Asynchronous Programming Model)。這種模式主要由一對Begin/End開頭的組成。BeginXXX方法用于啟動一個耗時操作(需要異步執(zhí)行的代碼段),相應的調(diào)用EndXXX來結(jié)束BeginXXX方法開啟的異步操作。BeginXXX方法和EndXXX方法之間的信息通過一個IAsyncResult對象來傳遞。這個對象是BeginXXX方法的返回值。如果直接調(diào)用EndXXX方法,則將以阻塞的方式去等待異步操作完成。另一種更好的方法是在BeginXXX倒數(shù)第二個參數(shù)指定的回調(diào)函數(shù)中調(diào)用EndXXX方法,這個回調(diào)函數(shù)將在異步操作完成時被觸發(fā),回調(diào)函數(shù)的第二個參數(shù)即EndXXX方法所需要的IAsyncResult對象。
.NET中一個典型的例子如System.Net命名空間中的HttpWebRequest類里的BeginGetResponse和EndGetResponse這對方法:
IAsyncResult BeginGetResponse(AsyncCallback callback, object state) WebResponse EndGetResponse(IAsyncResult asyncResult)
由方法聲明即可看出,它們符合前述的模式。
APM使用簡單明了,雖然代碼量稍多,但也在合理范圍之內(nèi)。APM兩個最大的缺點是不支持進度報告以及不能方便的“取消”。
EAP
在C# .NET第二個版本中,增加了一種新的異步編程模型EAP(Event-based Asynchronous Pattern),EAP模式的異步代碼中,典型特征是一個Async結(jié)尾的方法和Completed結(jié)尾的事件。XXXCompleted事件將在異步處理完成時被觸發(fā),在事件的處理函數(shù)中可以操作異步方法的結(jié)果。往往在EAP代碼中還會存在名為CancelAsync的方法用來取消異步操作,以及一個ProgressChenged結(jié)尾的事件用來匯報操作進度。通過這種方式支持取消和進度匯報也是EAP比APM更有優(yōu)勢的地方。通過后文TAP的介紹,你會發(fā)現(xiàn)EAP中取消機制沒有可延續(xù)性,并且不是很通用。
.NET2.0中新增的BackgroundWorker可以看作EAP模式的一個例子。另一個使用EAP的例子是被HttpClient所取代的WebClient類(新代碼應該使用HttpClient而不是WebClient)。WebClient類中通過DownloadStringAsync方法開啟一個異步任務,并有DownloadStringCompleted事件供設置回調(diào)函數(shù),還能通過CancelAsync方法取消異步任務。
TAP & async/await
從.NET4.0開始新增了一個名為TPL的庫主要負責異步和并行操作的處理,目標就是使異步和并發(fā)操作有個統(tǒng)一的操作界面。TPL庫的核心是Task類,有了Task幾乎不用像之前版本的異步和并發(fā)那樣去和Thread等底層類打交道,作為使用者的我們只需要處理好Task,Task背后有一個名為的TaskScheduler的類來處理Task在Thread上的執(zhí)行。可以這樣說TaskScheduler和Task就是.NET4.0中異步和并發(fā)操作的基礎,也是我們寫代碼時不二的選擇。
對于Task可以將其理解為一個包裝委托對象(通常就是Action或Func對象)并執(zhí)行的容器,從Task對象的創(chuàng)建就可以看出:
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參數(shù)真蛋疼,很不容易區(qū)分重載,把參數(shù)都寫上吧。
執(zhí)行這個Task對象需要手動調(diào)用Start方法:
task1.Start();
這樣task對象將在默認的TaskScheduler調(diào)度下去執(zhí)行,TaskScheduler使用線程池中的線程,至于是新建還是使用已有線程這個對用戶是完全透明的。還也可以通過重載函數(shù)的參數(shù)傳入自定義的TaskScheduler。
關于TaskScheduler的調(diào)度,推薦園子里這篇文章,前半部分介紹了一些線程執(zhí)行機制,很值得一度。
當我們用new創(chuàng)建一個Task對象時,創(chuàng)建的對象是Created狀態(tài),調(diào)用Start方法后將變?yōu)閃aitingToRun狀態(tài)。至于什么時候開始執(zhí)行(進入Running狀態(tài),由TaskScheduler控制,)。Task的創(chuàng)建執(zhí)行還有一種“快捷方式”,即Run方法:
Task.Run(() => Console.WriteLine("Hello World"));
var txt = await Task<string>.Run(() => "Hello World");
這種方式創(chuàng)建的Task會直接進入WaitingToRun狀態(tài)。
Task的其他狀態(tài)還有RanToCompletion,Canceled以及Faulted。在到大RanToCompletion狀態(tài)時就可以獲得Task<T>類型任務的結(jié)果。如果Task在狀態(tài)為Canceled的情況下結(jié)束,會拋出 OperationCanceledException。如果以Faulted狀態(tài)結(jié)束,會拋出導致任務失敗的異常。
Task同時服務于并發(fā)編程和異步編程(在Jeffrey Richter的CLR via C#中分別稱這兩種模式為計算限制的異步操作和IO限制的異步操作,仔細想想這稱呼也很貼切),這里主要討論下Task和異步編程的相關的機制。其中最關鍵的一點就是Task是一個awaitable對象,這是其可以用于異步編程的基礎。除了Task,還有很多類型也是awaitable的,如ConfigureAwait方法返回的ConfiguredTaskAwaitable、WinRT平臺中的IAsyncInfo(這個后文有詳細說明)等。要成為一個awaitable類型需要符合哪些條件呢?其實就一點,其中有一個GetAwaiter()方法,該方法返回一個awaiter。那什么是awaiter對象呢?滿足如下3點條件即可:
-
實現(xiàn)INotifyCompletion或ICriticalNotifyCompletion接口
-
有bool類型的IsCompleted屬性
-
有一個GetResult()來返回結(jié)果,或是返回void
awaitable和awaiter的關系正如IEnumerable和IEnumerator的關系一樣。推而廣之,下面要介紹的async/await的幕后實現(xiàn)方式和處理yield語法糖的實現(xiàn)方式差不多。
Task類型的GetAwaiter()返回的awaiter是TaskAwaiter類型。這個TaskAwaiter很簡單基本上就是剛剛滿足上面介紹的awaiter的基本要求。類似于EAP,當異步操作執(zhí)行完畢后,將通過OnCompleted參數(shù)設置的回調(diào)繼續(xù)向下執(zhí)行,并可以由GetResult獲取執(zhí)行結(jié)果。
簡要了解過Task,再來看一下本節(jié)的重點 - async異步方法。async/await模式的異步也出來很久了,相關文章一大片,這里介紹下重點介紹下一些不容易理解和值得重點關注的點。我相信我曾經(jīng)碰到的困惑也是很多人的遇到的困惑,寫出來和大家共同探討。
語法糖
對async/await有了解的朋友都知道這兩個關鍵字最終會被編譯為.NET中和異步相關的狀態(tài)機的代碼。這一部分來具體看一下這些代碼,了解它們后我們可以更準確的去使用async/await同時也能理解這種模式下異常和取消是怎樣完成的。
先來展示下用于分析反編譯代碼的例子,一個控制臺項目的代碼,這是能想到的展示異步方法最簡單的例子了,而且和實際項目中常用的代碼結(jié)構(gòu)也差不太多:
//實體類
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函數(shù)中使用了task.Result來獲取異步結(jié)果,需要注意這是一種阻塞模式,在除控制臺之外的UI環(huán)境不要使用類似Result屬性這樣會阻塞的方法,它們會導致UI線程死鎖。而對于沒有SynchronizationContext的控制臺應用確是再合適不過了。對于沒有返回值的Task,可以使用Wait()方法等待其完成。
這里使用ILSpy去查看反編譯后的代碼,而且注意要將ILSpy選項中的Decompile async methods (async/await)禁用(如下圖),否則ILSpy會很智能將IL反編譯為有async/await關鍵字的C#代碼。另外我也嘗試過Telerik JustDecompile等工具,但是能完整展示反編譯出的狀態(tài)機的只有ILSpy。

圖2
另外注意,應該選擇Release版本的代碼去查看,這是在一個Stackoverflow回答中看到的,說是有啥不同,具體也沒仔細看,這里知道選擇Release版exe/dll反編譯就好了。下面以Service類為例來看一下反編譯后的代碼:

圖3
通過圖上的注釋可以看到代碼主要由兩大部分構(gòu)成,Service類原有的代碼和一個由編譯器生成的狀態(tài)機,下面分別具體了解下它們都做了什么。依然是以圖片加注釋為主,重要的部分會在圖后給出文字說明。

圖4
通過上圖中的注釋可以大致了解GetUserName方法編譯后的樣子。我們詳細介紹下其中幾個點,首先是AsyncTaskMethodBuilder<T>,我感覺很有必要列出其代碼一看:
為了篇幅關系,這里刪除了部分復雜的實現(xiàn),取而代之的是介紹方法作用的注釋性文字,對于簡單的方法或是重要的方法保留了代碼。
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>);
}
//開始狀態(tài)機的執(zhí)行
[__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;
//這是整個方法乃至類中最重要的一部分
//獲取當前狀態(tài)執(zhí)行完畢后下一步的操作
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)
{
//設置結(jié)果
//通過Task上的方法來完成
}
internal void SetResult(Task<TResult> completedTask)
{
//設置結(jié)果,調(diào)用上面的方法來完成
}
public void SetException(Exception exception)
{
//設置異常
//通過Task上的方法來實現(xiàn)
}
internal void SetNotificationForWaitCompletion(bool enabled)
{
this.Task.SetNotificationForWaitCompletion(enabled);
}
private Task<TResult> GetTaskForResult(TResult result)
{
//獲取Task包裝的結(jié)果
}
}
}
狀態(tài)機的幾種狀態(tài)如下:
-
-1:表示還未開始執(zhí)行
-
-2:執(zhí)行結(jié)束,可能是正常完成,也可能遇到異常處理異常后結(jié)束
-
0~:下一個狀態(tài)。如0表示初始的-1之后的下一個狀態(tài),1表示0后的下一狀態(tài),以此類推。
上面的類中還出現(xiàn)了一個很重要的類型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()
{
//這個方法被包裝為“繼續(xù)執(zhí)行”委托實際執(zhí)行的代碼
//這個方法最終要的作用是給繼續(xù)執(zhí)行的代碼設置正確的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)
{
}
//上文提到的獲取“繼續(xù)執(zhí)行”委托的方法
//方法通過包裝內(nèi)部類MoveNextRunner的Run方法來實現(xiàn)
[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中的狀態(tài)機變量。這里發(fā)生裝箱操作。
}
internal static void ThrowAsync(Exception exception, SynchronizationContext targetContext)
{
//將異常與SynchronizationContext相關聯(lián)
}
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)
{
//獲取用于調(diào)試目的的“繼續(xù)執(zhí)行”委托
}
internal static Task TryGetContinuationTask(Action action)
{
//獲取“繼續(xù)執(zhí)行”的Task
}
}
}
總結(jié)來說AsyncTaskMethodBuilder<T>和AsyncMethodBuilderCore控制著狀態(tài)機的執(zhí)行(主要是在正確的Context下調(diào)用MoveNext方法),并在執(zhí)行狀態(tài)機的過程中負責正確的設置ExecutionContext和SynchronizationContext。
介紹了這么多基礎構(gòu)造,你可能更關心原來的調(diào)用Repository的方法的代碼去哪了,它們在狀態(tài)機的代碼中。下面就來看一下狀態(tài)機:

圖5
通過注釋應該可以了解這個狀態(tài)機的細節(jié)了。
簡單的說一下這個struct優(yōu)化。一開始狀態(tài)機被作為struct對象放置在棧上,對于await的工作已經(jīng)完成不需要等待的情況,將快速結(jié)束狀態(tài)機,這樣狀態(tài)機直接出棧效率高。如果await的工作需要等待則控制異步方法執(zhí)行的AsyncTaskMethodBuilder再將狀態(tài)機移動到堆中。因為這種情況下會發(fā)生Context切換(在SynchronizationContext不為空的情況下),如果狀態(tài)機還在棧上則會導致很大的切換負擔。
其實搞成一個狀態(tài)機的目的主要還是考慮到可能存在多個await的情況。對于只有1個await的情況其實狀態(tài)機的必要性不大,幾個if也就夠了,下面擴展下上面的例子看看有2個以上await(1個和2個await的狀態(tài)機都是使用if/else解決問題,從3個起開始不同)時編譯器產(chǎn)生的代碼,首先是擴展后的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將把函數(shù)體分割為4個異步區(qū)間,如下:

圖6
編譯生成的代碼最主要的不同是生成的狀態(tài)機變了,依舊是通過截圖和注釋來說一下這個新的狀態(tài)機的執(zhí)行情況(方便對比,注釋將只標出與之前狀態(tài)機不同的部分):

圖7
通過上面的分析,async/await關鍵字背后的秘密已經(jīng)清清楚楚。下面來說一下線程的問題。
線程!
關于async/await模式線程的問題,剛開始學習async/await那陣,看到很多文章,各種各樣的說法,一度讓我很迷惑。
一種觀點是很多國外同行的文章里說的:async/await本身不創(chuàng)建線程。StackoverFlow上很多回答也明確說async/await這兩個新增的關鍵字只是語法糖,編譯后的代碼不新建線程,這曾經(jīng)一度給我造成了很大的困惑:“不創(chuàng)建線程的話要異步還有啥用!”。
后來看到一種觀點是園友jesse2013博文中的一句話:
await 不會開啟新的線程,當前線程會一直往下走直到遇到真正的Async方法(比如說HttpClient.GetStringAsync),這個方法的內(nèi)部會用Task.Run或者Task.Factory.StartNew 去開啟線程。也就是如果方法不是.NET為我們提供的Async方法,我們需要自己創(chuàng)建Task,才會真正的去創(chuàng)建線程。
這個這個觀點應該是正確的,可后來看了很多代碼后感覺還不完全是這樣,畢竟一個被調(diào)用的async方法就會產(chǎn)生一個新的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;
}
}
在控制臺應用中執(zhí)行這段代碼會發(fā)現(xiàn)輸出的兩個線程Id是不相同的。
提示:控制臺引用程序沒有SynchronizationContext,在不恢復SynchronizationContext的情況下能更好的看出線程的變化。
到底情況是怎樣的呢,這里試著分析下我的想法:
這里先闡釋清“創(chuàng)建新線程”這個概念。我認為在這種情況下大家說的“創(chuàng)建新線程”可以被認為是與調(diào)用方法使用不同的線程,這個線程可能是線程池已有的,也可能是新建并被加入到線程池的線程。明確這給之后,繼續(xù)說線程問題。
首先肯定一點async/await關鍵字不會創(chuàng)建新線程是對的。如上文代碼中所示async/await被編譯為一個狀態(tài)機的確不參與Task的創(chuàng)建,實際新建Task的是被調(diào)用的異步方法。也就是說每調(diào)用一次異步方法(每一個await)都會產(chǎn)生一個新的Task,這個Task會自動執(zhí)行。前面說過Task由TaskScheduler安排執(zhí)行,一般都會在一個與調(diào)用線程不同的線程上執(zhí)行。
為了把這個問題解釋清楚,假設調(diào)用異步方法的線程為A,異步方法啟動后在B線程執(zhí)行。當B線程開始執(zhí)行后,A線程將交出控制權(quán)。異步方法執(zhí)行結(jié)束后,后續(xù)代碼(await后面的代碼)將在B線程上使用A線程的ExecutionContext(和SynchronizationContext,默認情況)繼續(xù)執(zhí)行。
注意這個A線程到B線程控制權(quán)的轉(zhuǎn)換正是async異步模式的精髓之一。在WPF等這樣的客戶端環(huán)境這樣做不會阻塞UI線程,使界面不失去響應。在MVC這樣的Web環(huán)境可以及時釋放HTTP線程,使Web服務器可以接收更多請求。畢竟B線程這種線程池中的線程成本更低。這樣就是為什么既然也要花等待異步操作完成的時間,還要另外使用異步方法的原因 - 及時釋放調(diào)用線程,讓低成本的線程去處理耗時的任務。
最后當需要在發(fā)起執(zhí)行的線程(這里是A線程)上繼續(xù)進行處理時只要獲得當時A線程的ExecutionContext和SynchronizationContext就可以了,并在這些Context完成剩余操作即可。
如果后續(xù)還有其他await,則會出現(xiàn)C線程,D線程等。如B調(diào)用了C的話,B的各種Context會被傳遞給C。當從異步方法返回后,執(zhí)行的線程變了但是Context沒變。這樣異步方法給我們的感覺就像是同步一般。這也就是async/await方法的精妙之處。
那個Task的ConfigureAwait方法又是做什么用的呢,理解了上文就很好理解這個方法了。在異步方法返回時,會發(fā)生線程切換,默認情況下(ConfigureAwait(true)時)ExecutionContext和SynchronizationContext都會被傳遞。如果ConfigureAwait(false)則只有ExecutionContext會被傳遞,SynchronizationContext不會被傳遞。在WPF等客戶端程序UI部分,應該使用默認設置讓SynchronizationContext保持傳遞,這樣異步代碼的后續(xù)代碼才能正常操作UI。除此之外的其他情況,如上面的Service類中,都該使用ConfigureAwait(false)以放棄SynchronizationContext的傳遞來提高性能。
下面以圖應該會對上面這段文字有更深的了解:
吐槽一下,本來是想用vs生成的時序圖進行演示呢。結(jié)果發(fā)現(xiàn)vs2015取消這個功能了。手頭也沒有其他版本的vs。就用代碼截圖來掩飾這個線程變化過程吧。
首先是控制臺程序的線程變化情況:

圖8
因為控制臺應用沒有SynchronizationContext,所以可以清楚的看到線程的變化。
下面看看在WPF中類似流程執(zhí)行的樣子:

圖9
可以看到在默認情況下每個await后的異步代碼返回到都回到UI線程,即所有await的后繼代碼都使用UI線程的SynchronizationContext來執(zhí)行。除了調(diào)用方法外,其它所有的方法沒有必要返回UI線程,所以我們應該把除調(diào)用開始處(即Button_Click方法)外的所有異步調(diào)用都配置為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環(huán)境的不同,是否恢復SynchronizationContext的影響。對于ASP.NET環(huán)境雖然也有SynchronizationContext,但實測線程切換的表現(xiàn)比較詭異,實在無法具體分析,但按照WPF的方式來配置異步肯定是對的。
其它資料:據(jù)CLR via C#作者大神Jeffrey Richter在書中所說,.NET這種以狀態(tài)機實現(xiàn)異步的思想來自于其為.NET 4.0寫的Power Threading庫中的AsyncEnumerator類。可以將其作為一個參考來學習async異步方法的機制。
async異步編程中的取消和進度報告
由文章開始處的圖1可知,Task天生支持取消,通過一個接收CancellationToken的重載創(chuàng)建的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異步編程最大的一個特點就是傳播性,即如果有一個異步方法,則所有調(diào)用這個方法的方法都應該是異步方法,而不能有任何同步方法(控制臺應用Main函數(shù)中那種把異步轉(zhuǎn)同步的方式除外)。而通過CancellationToken實現(xiàn)的取消模式可以很好的適配這種傳播性,所需要做的就是把所有異步方法都添加支持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()調(diào)用,這是可以及時取消后續(xù)未完成代碼的關鍵。當執(zhí)行這個語句時,如果ct被標記取消,則這個語句拋出OperationCanceledException異常,后續(xù)代碼停止執(zhí)行。
和取消機制一樣,新版的.NET也為進度通知提供了內(nèi)置類型的支持。IProgress<T>和Progress<T>就是為此而生。類型中的泛型參數(shù)T表示Progress的ProgressChanged事件訂閱的處理函數(shù)的第二個參數(shù)的類型。擴展之前的例子,把它改成支持進度報告的方法:
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可以進一步傳遞,但注意進度值要在合理范圍內(nèi)
var email = user.Email;
progress.Report(50);//報告進度
var avatar = await _webHelpler.GetAvatarByEmailAsync(email, progress);
progress.Report(100);
return avatar;
}
}
可以看到在async異步模式下取消和進度都很容易使用。
以上介紹了擁有async/await支持的TAP異步編程。在編寫新的異步代碼時應該優(yōu)先選用TAP模型,而且新版的.NET庫幾乎給所有同步接口增加了這種可以通過async/await使用的異步接口。但往往項目中會存在一些使用APM或EAP模式的代碼,通過下面介紹的一些方法可以使用async/await的方式調(diào)用這些代碼。
將BeginXXX/EndXXX的APM模式代碼轉(zhuǎn)為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>來構(gòu)造Task對象。
封裝EAP模式的代碼要比APM麻煩一些,我們需要手動構(gòu)造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結(jié)果來構(gòu)造Task的方式。
上面寫了那么多,真沒有信息保證全部都是正確的。最后推薦3篇文章,相信它們對理解async異步方法會有很大幫助,本文的很多知識點也是來自這幾篇文章:
WinRT 異步編程 C#
WinRT是完全不同于.NET的一種框架,目地就是把Windows的底層包裝成API讓各種語言都可以簡單的調(diào)用。WinRT中對異步的實現(xiàn)也和.NET完全不同,這一小節(jié)先看一下WinRT中異步機制的實現(xiàn)方法,再來看一下怎樣使用C#和.NET與WinRT中的異步API進行交互。
前文提到async異步編程中兩個比較重要的對象是awaitable和awaiter。在WinRT中充當awaitable的是IAsyncInfo接口的對象,具體使用中有如下4個實現(xiàn)IAsyncInfo接口的類型:
-
IAsyncAction
-
IAsyncActionWithProgress<TProgress>
-
IAsyncOperation<TResult>
-
IAsyncOperationWithProgress<TResult, TProgress>
由泛型參數(shù)可以看出Action和Operation結(jié)尾的兩個類型不同之處在于IAsyncAction的GetResults方法返回void,而IAsyncOperation<TResult>的GetResults方法返回一個對象。WithProgress結(jié)尾的類型在類似類型的基礎上增加了進度報告功能(它們內(nèi)部定義了Progress事件用來執(zhí)行進度變更時的處理函數(shù))。
Task和IAsyncInfo分別是對.NET和WinRT中異步任務的包裝。它們的原理相同但具體實現(xiàn)有所不同。IAsyncInfo表示的任務的狀態(tài)(可以通過Status屬性查詢)有如下幾種(和Task對照,整理自MSDN):
|
Task狀態(tài) (TaskStatus類型) |
IAsyncInfo狀態(tài) (AsyncStatus類型) |
|
RanToCompletion |
Completed |
|
Faulted |
Error |
|
Canceled |
Canceled |
|
所有其他值和已請求的取消 |
Canceled |
|
所有其他值和未請求的取消 |
Started |
另外獲取異常的方式也不一樣,通過Task中的Exception屬性可以直接得到.NET異常,而IAsynInfo中錯誤是通過ErrorCode屬性公開的一個HResult類型的錯誤碼。當時用下文價紹的方法將IAsynInfo轉(zhuǎn)為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。這個方法的實現(xiàn)其實也很簡單(以第一個重載為例):
public static TaskAwaiter GetAwaiter(this IAsyncAction source)
{
return WindowsRuntimeSystemExtensions.AsTask(source).GetAwaiter();
}
可以看到就是通過task.GetAwaiter得到的TaskAwaiter對象。
這一系列擴展方法的背后又有一個更重要的擴展方法 - AsTask()。
AsTask方法有更多的重載,其實現(xiàn)原理和前文介紹將EAP包裝為async異步模式的代碼差不多,都是通過TaskCompletionSource來手工構(gòu)造Task。下面展示的是一個最復雜的重載的實現(xiàn):
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;
}
}
通過參數(shù)可以看到,這個轉(zhuǎn)換Task的過程支持調(diào)用方法傳入的取消和進度報告。如果我們需要調(diào)用的WinRT異步方法的過程中支持取消和進度報告,就不能直接await那個異步方法(相當于調(diào)用了默認無參的AsTask的返回task上的GetAwaiter方法),而是應該await顯示調(diào)用的AsTask(可以傳入CancellationToken及IProgress參數(shù)的重載,上面那個)返回的task對象。這個可以見本小節(jié)末尾處的例子。
回頭看一下上面給出的AsTask的實現(xiàn)。里面一個最終要的對象就是TaskToAsyncOperationWithProgressAdapter<TResult, TProgress>,其可以由IAsyncOperationWithProgress<TResult, TProgress>直接轉(zhuǎn)型而來。它也是IAsyncOperationWithProgress<TResult, TProgress>和Task之間的一個橋梁。這個類的工作主要由其父類TaskToAsyncInfoAdapter<TCompletedHandler, TProgressHandler, TResult, TProgressInfo>來完成。這個父類的實現(xiàn)就比較復雜了,但道理都是相同的。有興趣的同學自行查看其實現(xiàn)吧。
了解了原理最后來看一下代碼示例,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#調(diào)用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的恢復。
不知道參數(shù)ct和progress怎么來的同學可以看上一小節(jié)的取消和異步部分。
除了由IAsyncInfo到Task的轉(zhuǎn)換外,還可以由Task/Task<T>轉(zhuǎn)為IAsyncAction/IAsyncOperation<T>。這個轉(zhuǎn)換的主要作用是把C#寫的代碼封裝為WinRT供其它語言調(diào)用。實現(xiàn)這個操作的AsAsyncAction/AsAsyncOperation<T>方法也是定義于上面提到的System.Runtime.WindowsRuntime.dll程序集中。以本文第一小節(jié)的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方法支持多種不同類型的委托對象作為參數(shù),比較復雜的一種可以支持取消和進度報告作為委托對象(一般是lambda表達式)的參數(shù),比如把上面的例子改成支持取消和進度報告后如下:
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;
}
}
內(nèi)幕這樣就輕松的實現(xiàn)了將C#編寫的代碼作為WinRT組件的過程。從如下AsAsyncOperation和AsyncInfo.Run的反編譯代碼來看,很難知道這個方法的實現(xiàn)細節(jié),畢竟它們都是和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++實現(xiàn)類似C#中基于Task的線程管理方式,另一方面讓C++(準確說是C++/CX)可以實現(xiàn)與WinRT規(guī)范的的異步接口互操作。
這些擴展主要定義于ppltask.h中,concurrency命名空間下。
concurrency::task
先來看一下和.NET Task基本等價的task類型。這也是微軟C++擴展中并發(fā)異步線程管理的核心類型之一。微軟圍繞concurrency::task的設計的一些方法與C#中的Task相關方法真的非常下。下面的表格對比了C#的Task與C++中的concurrency::task。有C# Task基礎的話,對于concurrency::task很容易就能上手。
| C# Task | C++ concurrency::task | |
| 構(gòu)造 方式1 | constructor | constructor |
| 構(gòu)造 方式2 | Task.Factory.StartNew() |
用于異步 - create_task() |
| 構(gòu)造 方式3 |
用于并行 - make_task() 返回task_handle,和task_group等同用。 |
|
| 阻塞 - 等待完成 | task.Wait() | task::wait() |
| 阻塞 - 等待獲取結(jié)果 | GetAwaiter().GetResult() | task::get() |
| 任務狀態(tài)類型 | TaskStatus | concurrency::task_status |
| 并行 - 等待全部 | Task.WhenAll() | concurrency::when_all |
| 并行 - 等待部分 | Task.WhenAny() | concurrency::when_any |
| 異步 - 任務延續(xù) | Task.ContinueWith() | task::then() |
接著討論一下本節(jié)的重點內(nèi)容,微軟給C++帶來的異步支持。
普通異步
看過之前介紹C#異步的部分,可以知道支持異步的系統(tǒng)無非就由以下以下幾部分組成:任務創(chuàng)建、任務延續(xù)、任務等待、取消、進度報告等。依次來看一下ppltask.h中支持這些部分的方法。
create_task方法可以將函數(shù)對象(廣義上的函數(shù)對象包含如lambda表達式,在C++11中也多用lambda表達式作為函數(shù)對象)包裝成task類對象。如上文所述,定義在ppltask.h中,位于concurrency命名空間下的task類和異步方法關系最密切。下面的代碼示例了concurrency::task的創(chuàng)建。
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關鍵字的支持,所以后續(xù)任務不能像C#中那樣直接跟在await ...語句后,必須通過task::then方法來設置。
then方法也可以實現(xiàn)鏈式調(diào)用,如:
auto t = create_task([]()
{
//do something
}).then([](int v){
return 0;
});
關于后續(xù)代碼執(zhí)行上下文的問題,如果create_task方法接受的函數(shù)對象返回的是task<T>或task<void>則后續(xù)代碼會在相同的線程上下文運行,如果返回的是T或void則后續(xù)任務會在任意上下文運行。可以使用concurrency::task_continuation_context來更改這個設置。具體用法是將task_continuation_context傳給task::then其中那些接受task_continuation_context類型參數(shù)的重載。如果參數(shù)值為concurrency::task_continuation_context::use_arbitrary,則表示指定延續(xù)在后臺線程上運行,如果參數(shù)值為concurrency::task_continuation_context::use_current,則表示指定延續(xù)在調(diào)用了task::then的線程上運行。如:
auto t = create_task([]()
{
//do something
}).then([](int v){
//do something else;
},task_continuation_context::use_arbitrary());//then()中傳入的代碼將在后臺線程執(zhí)行,相對于C#中配置ConfigAwait(false)。
對于取消和異步的支持,將在下一小段進行介紹,那里的實現(xiàn)方式同樣可以應用到這一部分中。
使用create_task的方式創(chuàng)建task的方法只用于C++內(nèi)部對task的管理。如果是希望將異步作為WinRT組件發(fā)布需要使用下面介紹的create_async。
如果是純C++中處理多線程任務,除了使用Windows中所提供的task,還可以考慮C++11標準庫中的thread,后者跨平臺更好。后文會有一部分介紹C++11的thread。如果是對C#的TPL模型很熟悉,轉(zhuǎn)到C++使用ppltask.h中的task會發(fā)現(xiàn)模型一致性很高。
支持WinRT的異步
1. 提供WinRT標準的異步方法
通過create_async方法可以將函數(shù)轉(zhuǎn)為異步函數(shù),即這個方法是返回IAsyncInfo對象的。通過這個方法可以將代碼包裝成WinRT中標準的異步方法供其它語言調(diào)用。被包裝的代碼一般是可調(diào)用對象,在C++11中一般都使用Lambda表達式。返回的IAsyncInfo的具體類型(上文介紹的四種之一)是有傳入的參數(shù)決定的。
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的函數(shù)對象的方法體是一般的代碼。還以把create_task方法的調(diào)用傳入create_async接收的lambda表達式的方法體中,create_task返回的concurrency::task也可以配置一系列的then(),最終這些配置都將反應給最外部的create_async的包裝。
下面的代碼就是包裝了最簡單的過程代碼:
IAsyncOperation<int>^ op2 = create_async([]()
{
return 0;
});
也可以像上面說的包裝一段create_task的代碼(把C++內(nèi)部的任務暴露給WinRT接口):
IAsyncOperation<int>^ op3 = create_async([](){
return create_task(KnownFolders::DocumentsLibrary->GetFileAsync("Dictionary.txt")).then([](StorageFile^ file)
{
int wordNum = 0;
// 獲取單詞數(shù)
return wordNum;
};
});
通過create_async的重載也可以輕松的支持取消和進度報告。
擴展的C++使用的異步模式與C# TPL使用的標記式取消模型一致,但在使用上還是稍有不同,在介紹這種模式之前,先來說說取消延續(xù)的問題,如下面的代碼:
auto t1 = create_task([]() -> int
{
//取消任務
cancel_current_task();
});
auto t2 = t1.then([](task<int> t)
{
try
{
int n = t.get();
wcout << L"后續(xù)任務" << endl;
}
catch (const task_canceled& e)
{
}
});
auto t3 = t1.then([](int n)
{
wcout << L"后續(xù)任務" << endl;
});
這個例子中可以看到,我們可以在task內(nèi)部方法中通過cancel_current_task()調(diào)用來取消當前的任務。如果t1被手動取消,對于t1的兩個后繼任務t2和t3,t2會被取消,t3不會被取消。這是由于t2是基于值延續(xù)的延續(xù),而t3是基于任務的延續(xù)。
接下來的示例展示了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([]{
// 延續(xù)任務
},token,concurrency::task_continuation_context::use_current);//傳遞取消標記,接收取消標記的重載還需要延續(xù)上下文的參數(shù)
// 觸發(fā)取消
cts.cancel();
t.wait();
通過使用cancellation_token,取消也可以傳遞到基于任務的延續(xù)。
上面演示的例子cancellation_token是在create_async方法內(nèi)部定義的,更常見的情況在create_async的工作方法參數(shù)中顯示聲明cancellation_token并傳入到工作方法內(nèi),這樣IAsyncXXX上面的Cancel方法被調(diào)用,取消標志也會被自動設置,從而觸發(fā)鏈式的標記性取消。
說起來很抽象,可以參考下面的代碼:
IAsyncAction^ DoSomething(){
return create_async([](cancellation_token ct)
{
auto t = create_task([ct]()
{
// do something
});
});
}
這樣當DoSomething返回值(IAsyncAction對象)的Cancel方法被調(diào)用后,ct被標記為取消,任務t會在合適的時間被取消執(zhí)行。
C++的cancellation_token有一個更高級的功能:其上可以設置回調(diào)函數(shù),當cts觸發(fā)取消時,token被標記為取消時,會執(zhí)行這個回調(diào)函數(shù)的代碼。
cancellation_token_registration cookie;
cookie = token.register_callback([&e, token, &cookie]()
{
// 記錄task被取消的日志等
// 還可以取消注冊的回調(diào)
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>對象當作參數(shù)傳入create_async接收的工作函數(shù)。然后就可以使用reporter的report方法來報告進度。返回的IAsyncOperationWithProgress類型可以使這個進度報告與WinRT中調(diào)用這個方法的代碼協(xié)同工作。
2. 調(diào)用WinRT標準的異步方法
說了創(chuàng)建異步方法,再來看看使用C++調(diào)用WinRT的異步方法。由于C++中沒有async/await那樣的異步模式,所以最值得關心的就是如何,所以當一個任務完成后需要手動傳入剩余的代碼來繼續(xù)后續(xù)任務的執(zhí)行,這里需要用到task的then方法,首先我們需要把IAsyncInfo轉(zhuǎn)為task。(其實上面的代碼已經(jīng)演示了這個用法)
不同于C#中通過AsTask方法將IAsyncInfo等類型轉(zhuǎn)為Task對象。C++中是使用create_task的方法(就是上面介紹的那個,不同的重載)來完成這個工作:
auto createFileTadk =create_task(folder->CreateFileAsync("aa.txt",CreationCollisionOption::ReplaceExisting));
接著調(diào)用task的then方法設置后續(xù)執(zhí)行:
createFileTadk.then([this](StorageFile^ storageFileSample) {
String^ filename=storageFileSample->Name;
});
捕獲異常方面,不涉及WinRT的部分遵循C++的異常捕獲原則,WinRT交互部分,需要保證拋出的異常可以被WinRT識別處理。
除了使用ppltask.h中的擴展,還可以使用WRL中的AsyncBase模板類來實現(xiàn)C++對WiinRT異步的支持。但后者的代碼過于晦澀,就不再介紹了。
說回來和WinRT交互就好用的語言還是C#,C++可以用于實現(xiàn)純算法部分,即位于WinRT下方的部分,只需要在必要的時候通過WinRT公開讓C#可調(diào)用的接口。這樣代碼的編寫效率和執(zhí)行效率都很高。另外C#的應用商店程序支持本地編譯也是大勢所趨,在WinRT之上使用C#或C++/CX區(qū)別不大。
C++ 11 線程&并發(fā)&異步
C++在沉寂多年之后,終于在新版標準中迎來爆發(fā),其中標準內(nèi)置的線程支持就是一個完全全新的特性。在之前版本的C++中沒有標準的線程庫,實現(xiàn)跨平臺的線程操作一般都要借助于第三方的庫。現(xiàn)在有了C++11,相同的操作線程的代碼可以在不同的編譯器上編譯執(zhí)行從而可以實現(xiàn)跨平臺的線程操作。
C++新標準中的線程,異步等看起來和C#的機制非常的像,不知道微軟和C++標準委員會誰“借鑒”的誰。
下面按線程,并發(fā)中同步支持,異步這樣的順序來逐個了解下C++新標準中增加的這些特性。介紹方式以C#的等價機制做對比,篇幅原因很多都是一個綱領作用,介紹一筆帶過,根據(jù)需要大家自行查找相應的功能的具體使用方法。
線程
C++11標準庫中引入了std::thread作為抽象線程的類型。其很多操作和.NET中的Thread類似。
| C++ 11 | C# | |
| std::thread | Thread | |
| 創(chuàng)建 | 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中內(nèi)建了互斥機制,可以讓多個線程安全的訪問同一個變量。幾種機制總結(jié)如下(可能并非完全一直,但效果上很類似)
| C++ 11 | C# | |
| 原子類型 |
atomic_type std::atomic<T> |
Interlocked |
| 內(nèi)存柵欄 | 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將一個可調(diào)用對象包裝廠一個異步方法,這個方法將返回一個std::future對象,通過std::future可以得到異步方法的結(jié)果。
看一下這段代碼(來自qicosmos老師的博文)就能明白上面所說:
std::future<int> f1 = std::async(std::launch::async, [](){
return 8;
});
cout<<f1.get()<<endl;
關于C++11異步方面的特性,強烈推薦qicosmos老師的博文以及他編寫的圖書《深入應用C++11:代碼優(yōu)化與工程級應用》。
C# 方法調(diào)用方信息
新版本的C#提供了方便獲取方法調(diào)用者信息的功能,對于需要調(diào)試以及輸出一些日志的情況很有用。這樣我們不需要像之前那樣在每個需要記錄日志的地方硬編碼下調(diào)用的方法名,提高了代碼的可讀性。
提供這個新功能的是幾個應用于參數(shù)的Attribute:
-
CallerFilePathAttribute 獲得調(diào)用方法所在的源文件地址
-
CallerLineNumberAttribute 被調(diào)用代碼的行號
-
CallerMemberNameAttribute 調(diào)用方法的名稱
使用其簡單只需要聲明一個參數(shù),然后把這些Attribute加在參數(shù)前面,在函數(shù)中取到的參數(shù)值就是我們想要的結(jié)果。一個簡單的例子如下:
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捕獲閉包外變量進行了一些小優(yōu)化,這個在之前文章介紹Lambda時有介紹,這里不再贅述。
C++ 調(diào)用方法信息
在C中就有宏來完成類似的功能。由于C++可以兼容C,所以在C++11之前,一般都用這種C兼容的方式來獲得被調(diào)用方法的信息。新版的C++對此進行了標準化,增加了一個名為__func__的宏來完成這個功能。
需要注意的是和C#中類似功能獲得調(diào)用方法名稱不同,這個__func__宏得到的是被調(diào)用方法,即__func__所在方法的名稱。個人感覺C++中__func__更實用。仍然是一個簡單的例子:
void Called()
{
std::cout << __func__ << std::endl;
}
void Caller()
{
Called();
}
調(diào)用Caller()將輸出"Called"。
C++中實現(xiàn)這個宏的方式就是在編譯過程中在每個方法體的最前面插入如下代碼:
static const char* __func__ = "Called";
了解這個之后你會感覺這個宏沒有那么神秘了。
除了新被標準化的__func__在大部分C++編譯器中仍然可以使用__LINE__和__FILE__獲取當前行號和所在文件。
預告
下篇文章將介紹C#6帶來的新特性,C#6中沒有什么重量級的改進(據(jù)說編譯器好像有很大改動,那個不了解就不說了,不是一般用戶能仔細研究的。編譯前端和編譯后端發(fā)展這么多年復雜程度接近操作系統(tǒng)了),大都是一些語法糖,而且糖的數(shù)量還不少。歡迎繼續(xù)關注。
本文斷斷續(xù)續(xù)寫了很久,中間還出去玩了2周。有什么錯誤請指正。

浙公網(wǎng)安備 33010602011771號