C#与C++的发展历程第三 - C#5.0异步编程巅峰
系列文章目录
3. C#与C++的发展历程第三 - C#5.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两个最大的缺点是不支持进度报告以及不能方便的“取消”。
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 异步编程 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周。有什么错误请指正。