C#与C++的发展历程第三 - C#5.0异步编程巅峰

系列文章目录html

1. C#与C++的发展历程第一 - 由C#3.0起前端

2. C#与C++的发展历程第二 - C#4.0再接再砺web

3. C#与C++的发展历程第三 - C#5.0异步编程的巅峰算法

 

C#5.0做为第五个C#的重要版本,将异步编程的易用度推向一个新的高峰。经过新增的async和await关键字,几乎可使用编写同步代码的方式来编写异步代码。编程

本文将重点介绍下新版C#的异步特性以及部分其余方面的改进。同时也将介绍WinRT程序一些异步编程的内容。c#

 

C# async/await异步编程

写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这对方法:

1
2
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对象的建立就能够看出:

1
2
3
4
5
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方法:

1
task1.Start();

这样task对象将在默认的TaskScheduler调度下去执行,TaskScheduler使用线程池中的线程,至因而新建仍是使用已有线程这个对用户是彻底透明的。还也能够经过重载函数的参数传入自定义的TaskScheduler。

关于TaskScheduler的调度,推荐园子里这篇文章,前半部分介绍了一些线程执行机制,很值得一度。

当咱们用new建立一个Task对象时,建立的对象是Created状态,调用Start方法后将变为WaitingToRun状态。至于何时开始执行(进入Running状态,由TaskScheduler控制,)。Task的建立执行还有一种“快捷方式”,即Run方法:

1
2
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同时也能理解这种模式下异常和取消是怎样完成的。

先来展现下用于分析反编译代码的例子,一个控制台项目的代码,这是能想到的展现异步方法最简单的例子了,并且和实际项目中经常使用的代码结构也差不太多:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
//实体类
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>,我感受颇有必要列出其代码一看:

为了篇幅关系,这里删除了部分复杂的实现,取而代之的是介绍方法做用的注释性文字,对于简单的方法或是重要的方法保留了代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
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,简单的了解一下这个类型也颇有必要。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
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应用为例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
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可能去“开启一个新线程”。改造下上面的代码测试这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
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能够被通知取消。

1
2
3
4
var  tokenSource =  new  CancellationTokenSource();
CancellationToken ct = tokenSource.Token;
var  task = Task.Run(() => Task.Delay(10000,ct), ct);
tokenSource.Cancel();

天然咱们异步方法的取消也离不开CancellationToken,方法就是给异步方法添加接收CancellationToken的重载,如前文示例代码Service中的方法能够添加一个这样的重载支持取消:

1
2
3
4
public  async Task< byte []> GetUserAvatarAsync( int  id, CancellationToken ct)
{
     ...
}

async异步编程最大的一个特色就是传播性,即若是有一个异步方法,则全部调用这个方法的方法都应该是异步方法,而不能有任何同步方法(控制台应用Main函数中那种把异步转同步的方式除外)。而经过CancellationToken实现的取消模式能够很好的适配这种传播性,所须要作的就是把全部异步方法都添加支持CancellationToken的重载。以前的例子改形成支持取消后以下(展现一部分):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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事件订阅的处理函数的第二个参数的类型。扩展以前的例子,把它改为支持进度报告的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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为例:

1
2
3
4
public  Task<WebResponse> GetResponseAsync(WebRequest client)
{
     return  Task<WebResponse>.Factory.FromAsync(client.BeginGetResponse, client.EndGetResponse,  null );
}

TaskFactory的FromAsync方法中使用TaskCompletionSource<T>来构造Task对象。

封装EAP模式的代码要比APM麻烦一些,咱们须要手动构造TaskCompletionSource对象(代码来自,手打的)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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个):

1
2
public  static  TaskAwaiter GetAwaiter<TResult>( this  IAsyncAction source);
public  static  TaskAwaiter<TResult> GetAwaiter<TResult, TProgress>( this  IAsyncOperationWithProgress<TResult, TProgress> source);

咱们又见到了熟悉的TaskAwaiter。这个方法的实现其实也很简单(以第一个重载为例):

1
2
3
4
public  static  TaskAwaiter GetAwaiter( this  IAsyncAction source)
{
   return  WindowsRuntimeSystemExtensions.AsTask(source).GetAwaiter();
}

能够看到就是经过task.GetAwaiter获得的TaskAwaiter对象。

这一系列扩展方法的背后又有一个更重要的扩展方法 - AsTask()。

AsTask方法有更多的重载,其实现原理和前文介绍将EAP包装为async异步模式的代码差很少,都是经过TaskCompletionSource来手工构造Task。下面展现的是一个最复杂的重载的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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相关的类中只提供异步方法,示例所以也选择了这个使用最普遍的功能(示例代码来源是某开源库,具体是啥忘了,有轻微改动):

1
2
3
4
5
6
7
8
9
10
11
12
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中的异步接口同样的简单。

若是是须要传递取消和进度报告怎么办呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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>的方法,以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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表达式)的参数,好比把上面的例子改为支持取消和进度报告后以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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代码相关的部分。

1
2
3
4
5
6
7
8
9
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的建立。

1
2
3
4
task< int > op1 = create_task([]()  
{  
      return  0;  
});

在C++11中通常都使用auto直接表示一些复杂的类型,让编译器去推断。例子中写出完整的类型可让读者更好的理解方法的返回类型。

而相似于.NET Task中的ContinueWith方法的task::then方法,基本使用以下:

1
2
3
op1.then([]( int  v){  
      return  0;  
});

在C++中因为没有相似C#中async/await关键字的支持,因此后续任务不能像C#中那样直接跟在await ...语句后,必须经过task::then方法来设置。

then方法也能够实现链式调用,如:

1
2
3
4
5
6
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的线程上运行。如:

 

1
2
3
4
5
6
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的声明:

1
2
3
4
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的包装。

下面的代码就是包装了最简单的过程代码:

1
2
3
4
IAsyncOperation< int >^ op2 = create_async([]()  
{  
      return  0;  
});

也能够像上面说的包装一段create_task的代码(把C++内部的任务暴露给WinRT接口):

1
2
3
4
5
6
7
8
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使用的标记式取消模型一致,但在使用上仍是稍有不一样,在介绍这种模式以前,先来讲说取消延续的问题,以下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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++中 的标记式取消:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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方法被调用,取消标志也会被自动设置,从而触发链式的标记性取消。

提及来很抽象,能够参考下面的代码:

1
2
3
4
5
6
7
8
9
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被标记为取消时,会执行这个回调函数的代码。

1
2
3
4
5
6
7
8
cancellation_token_registration cookie;
cookie = token.register_callback([&e, token, &cookie]()
{
     // 记录task被取消的日志等
 
     // 还能够取消注册的回调
     token.deregister_callback(cookie);
});

说完取消,再来看一下进度报告。下面的例子基本是演示进度报告最简单的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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的方法(就是上面介绍的那个,不一样的重载)来完成这个工做:

1
auto  createFileTadk =create_task(folder->CreateFileAsync( "aa.txt" ,CreationCollisionOption::ReplaceExisting));

接着调用task的then方法设置后续执行:

1
2
3
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()

一段简单的综合示例代码:

1
2
3
4
5
6
7
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老师的博文)就能明白上面所说:

1
2
3
4
5
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加在参数前面,在函数中取到的参数值就是咱们想要的结果。一个简单的例子以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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__更实用。仍然是一个简单的例子:

1
2
3
4
5
6
7
8
9
void  Called()
{
     std::cout << __func__ << std::endl;
}
 
void  Caller()
{
     Called();
}

调用Caller()将输出"Called"。

C++中实现这个宏的方式就是在编译过程当中在每一个方法体的最前面插入以下代码:

1
static  const  char * __func__ =  "Called" ;

了解这个以后你会感受这个宏没有那么神秘了。

除了新被标准化的__func__在大部分C++编译器中仍然可使用__LINE__和__FILE__获取当前行号和所在文件。

 

预告

下篇文章将介绍C#6带来的新特性,C#6中没有什么重量级的改进(听说编译器好像有很大改动,那个不了解就不说了,不是通常用户能仔细研究的。编译前端和编译后端发展这么多年复杂程度接近操做系统了),大都是一些语法糖,并且糖的数量还很多。欢迎继续关注。

 

本文断断续续写了好久,中间还出去玩了2周。有什么错误请指正。

相关文章
相关标签/搜索