本专题概要

  • 引言

  • 什么是TAP——基于任务的异步模式介绍

  • 如何使用TAP——使用基于任务的异步模式来异步编程

  • TAP与APM或EAP可以转换吗?——与其他异步模式的转换

  • 小结


一、引言

  在上两个专题中我为大家介绍.NET 1.0中的APM和.NET 2.0中的EAP,在使用前面两种模式进行异步编程的时候,大家多多少少肯定会感觉到实现起来比较麻烦, 首先我个人觉得,当使用APM的时候,首先我们要先定义用来包装回调方法的委托,这样难免有点繁琐, 然而使用EAP的时候,我们又需要实现Completed事件和Progress事件,上面两种实现方式感觉都有点繁琐,同时微软也意思到了这点,所以在.NET 4.0中提出了一个新的异步模式——基于任务的异步模式,该模式主要使用System.Threading.Tasks.Task和Task<T>类来完成异步编程,相对于前面两种异步模式来讲,TAP使异步编程模式更加简单(因为这里我们只需要关注Task这个类的使用),同时TAP也是微软推荐使用的异步编程模式,下面就具体为大家分享下本专题的内容.

二、什么是TAP——基于任务的异步模式介绍

基于任务的异步模式(Task-based Asynchronous Pattern,TAP)之所以被微软所推荐,主要就它使用简单,基于任务的异步模式使用单个方法来表示异步操作的开始和完成,然而异步编程模型(APM)却要求BeginXxx和EndXxx两个方法来分别表示异步操作的开始和完成(这样使用起来就复杂了),然而,基于事件的异步模式(EAP)要求具有Async后缀的方法和一个或多个事件、事件处理程序和事件参数。看到这里,是不是大家都有这样一个疑问的——我们怎样区分.NET类库中的类实现了基于任务的异步模式呢? 这个识别方法很简单,当看到类中存在TaskAsync为后缀的方法时就代表该类实现了TAP, 并且基于任务的异步模式同样也支持异步操作的取消进度的报告的功能,但是这两个实现都不像EAP中实现的那么复杂,因为如果我们要自己实现EAP的类,我们需要定义多个事件和事件处理程序的委托类型和事件的参数(具体可以查看上一专题中的BackgroundWorker剖析部分),但是在TAP实现中,我们只需要通过向异步方法传入CancellationToken参数,因为在异步方法内部会对这个参数的IsCancellationRequested属性进行监控,当异步方法收到一个取消请求时,异步方法将会退出执行(具体这点可以使用反射工具查看WebClient的DownloadDataTaskAsync方法,同时也可以参考我后面部分自己实现基于任务的异步模式的异步方法。),在TAP中,我们可以通过IProgress<T>接口来实现进度报告的功能,具体实现可以参考我后面的程序部分。

目前我还没有找到在.NET 类库中实现了基于任务的异步模式的哪个类提供进度报告的功能,下面的将为大家演示这个实现,并且也是这个程序的亮点,同时通过自己实现TAP的异步方法来进一步理解基于任务的异步模式。

三、如何使用TAP——使用基于任务的异步模式来异步编程

看完上面的介绍,我们是不是很迫不及待想知道如何自己实现一个基于任务的异步模式的异步方法的,并且希望只需要这个方法就可以完成异步操作的取消和进度报告的功能的(因为EAP中需要实现其他的事件和定义事件参数类型,这样的实现未免过于复杂),下面就基于上专题中实现的程序用基于任务的异步模式来完成下。下面就让我们实现自己的异步方法(亮点为只需要一个方法就可以完成进度报告和异步操作取消的功能):

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
//  Download File
         // CancellationToken 参数赋值获得一个取消请求
         // progress参数负责进度报告
         private  void  DownLoadFile( string  url, CancellationToken ct, IProgress< int > progress)
         {
             HttpWebRequest request =  null ;
             HttpWebResponse response =  null ;
             Stream responseStream =  null ;
             int  bufferSize = 2048;
             byte [] bufferBytes =  new  byte [bufferSize];
             try
             {
                 request = (HttpWebRequest)WebRequest.Create(url);
                 if  (DownloadSize != 0)
                 {
                     request.AddRange(DownloadSize);
                 }
                 response = (HttpWebResponse)request.GetResponse();
                 responseStream = response.GetResponseStream();
                 int  readSize = 0;
                 while  ( true )
                 {
                     // 收到取消请求则退出异步操作
                     if  (ct.IsCancellationRequested ==  true )
                     {
                         MessageBox.Show(String.Format( "下载暂停,下载的文件地址为:{0}\n 已经下载的字节数为: {1}字节" , downloadPath, DownloadSize));
                         response.Close();
                         filestream.Close();
                         sc.Post((state) =>
                         {
                             this .btnStart.Enabled =  true ;
                             this .btnPause.Enabled =  false ;
                         },  null );
                         // 退出异步操作
                         break ;
                     }
                     readSize = responseStream.Read(bufferBytes, 0, bufferBytes.Length);
                     if  (readSize > 0)
                     {
                         DownloadSize += readSize;
                         int  percentComplete = ( int )(( float )DownloadSize / ( float )totalSize * 100);
                         filestream.Write(bufferBytes, 0, readSize);
                         // 报告进度
                         progress.Report(percentComplete);
                     }
                     else
                     {
                         MessageBox.Show(String.Format( "下载已完成,下载的文件地址为:{0},文件的总字节数为: {1}字节" , downloadPath, totalSize));
                         sc.Post((state) =>
                         {
                             this .btnStart.Enabled =  false ;
                             this .btnPause.Enabled =  false ;
                         },  null );
                         response.Close();
                         filestream.Close();
                         break ;
                     }
                 }  
             }
             catch  (AggregateException ex)
             {
                 // 因为调用Cancel方法会抛出OperationCanceledException异常
                 // 将任何OperationCanceledException对象都视为以处理
                 ex.Handle(e => e  is  OperationCanceledException);
             }
         }

这样只需要上面的一个方法,我们就可以完成上一专题中文件下载的程序,我们只需要在下载按钮的事件处理程序调用该方法和在暂停按钮的事件处理程序调用CancellationTokenSource.Cancel方法即可,具体代码为:

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
// Start DownLoad File
         private  void  btnStart_Click( object  sender, EventArgs e)
         {
             filestream =  new  FileStream(downloadPath, FileMode.OpenOrCreate);
             this .btnStart.Enabled =  false ;
             this .btnPause.Enabled =  true ;
             filestream.Seek(DownloadSize, SeekOrigin.Begin);
             // 捕捉调用线程的同步上下文派生对象
             sc = SynchronizationContext.Current;
             cts =  new  CancellationTokenSource();
             // 使用指定的操作初始化新的 Task。
             task =  new  Task(() => Actionmethod(cts.Token), cts.Token);
             // 启动 Task,并将它安排到当前的 TaskScheduler 中执行。
             task.Start();
             //await DownLoadFileAsync(txbUrl.Text.Trim(), cts.Token,new Progress<int>(p => progressBar1.Value = p));
         }
         // 任务中执行的方法
         private  void  Actionmethod(CancellationToken ct)
         {
             // 使用同步上文文的Post方法把更新UI的方法让主线程执行
             DownLoadFile(txbUrl.Text.Trim(), ct,  new  Progress< int >(p =>
                 {
                     sc.Post( new  SendOrPostCallback((result)=>progressBar1.Value=( int )result),p);
                 }));
         }
         // Pause Download
         private  void  btnPause_Click( object  sender, EventArgs e)
         {
             // 发出一个取消请求
             cts.Cancel();
         }

下面看看基于任务的异步模式的实现效果如何的,运行结果:

点击确定按钮之后,Download按钮会重新变成可用,此时我们可以继续点击Download按钮来下载进行下载,下载完成之后会下载完成弹出框,运行结果如下:

四、TAP与APM或EAP可以转换吗?——与其他异步模式的转换

从上面的程序代码我们可以清楚的发现——基于任务的异步模式确实比前面的两种异步模式更加简单使用,所以,从.NET Framework 4.0开始,微软推荐使用TAP来实现异步编程,这里就涉及之前用APM或EAP实现的程序如何迁移到用TAP实现的问题的,同时.NET Framwwork对他们之间的转换了也做了很好的支持。

4.1 将APM转换为TAP

System.Threading.Tasks命名空间中,有一个TaskFactory(任务工程)类,我们正可以利用该类的FromAsync方法来实现将APM转换为TAP,下面就用基于任务的异步模式来实现在异步编程模型博文中例子。

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
// 大家可以对比这两种实现方式
         #region 使用APM实现异步请求
         private  void  APMWay()
         {
             WebRequest webRq = WebRequest.Create( "http://msdn.microsoft.com/zh-CN/" );
             webRq.BeginGetResponse(result =>
             {
                 WebResponse webResponse =  null ;
                 try
                 {
                     webResponse = webRq.EndGetResponse(result);
                     Console.WriteLine( "请求的内容大小为: "  + webResponse.ContentLength);
                 }
                 catch  (WebException ex)
                 {
                     Console.WriteLine( "异常发生,异常信息为: "  + ex.GetBaseException().Message);
                 }
                 finally
                 {
                     if  (webResponse !=  null )
                     {
                         webResponse.Close();
                     }
                 }
             },  null );
         }
         #endregion
         #region 使用FromAsync方法将APM转换为TAP
         private  void  APMswitchToTAP()
         {
             WebRequest webRq = WebRequest.Create( "http://msdn.microsoft.com/zh-CN/" );
             Task.Factory.FromAsync<WebResponse>(webRq.BeginGetResponse, webRq.EndGetResponse,  null , TaskCreationOptions.None).
                 ContinueWith(t =>
                 {
                     WebResponse webResponse =  null ;
                     try
                     {
                         webResponse = t.Result;
                         Console.WriteLine( "请求的内容大小为: "  + webResponse.ContentLength);
                     }
                     catch  (AggregateException ex)
                     {
                         if  (ex.GetBaseException()  is  WebException)
                         {
                             Console.WriteLine( "异常发生,异常信息为: "  + ex.GetBaseException().Message);
                         }
                         else
                         {
                             throw ;
                         }
                     }
                     finally
                     {
                         if  (webResponse !=  null )
                         {
                             webResponse.Close();
                         }
                     }
                 });
         }
         #endregion

上面代码演示了使用APM的原始实现方式以及如何使用FromAsync方法把APM的实现方式转换为TAP的实现方法,把这两种方式放在一起,一是可以帮助大家做一个对比,使大家更容易明白APM与TAP的转换,二是大家也可以通过上面的对比明白TAP与APM的区别。

4.2 将EAP转化为TAP

处理APM可以升级为用TAP来实现外,对于EAP,我们同样可以对其转换为TAP的方式,下面代码演示了如何将EAP转换为TAP的实现方式:

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
#region 将EAP转换为TAP的实现方式
             // webClient类支持基于事件的异步模式(EAP)
             WebClient webClient =  new  WebClient();
             // 创建TaskCompletionSource和它底层的Task对象
             TaskCompletionSource< string > tcs =  new  TaskCompletionSource< string >();
             // 一个string下载好之后,WebClient对象会应发DownloadStringCompleted事件
             webClient.DownloadStringCompleted += (sender, e) =>
             {
                 // 下面的代码是在GUI线程上执行的
                 // 设置Task状态
                 if  (e.Error !=  null )
                 {
                     // 试图将基础Tasks.Task<TResult>转换为Tasks.TaskStatus.Faulted状态
                     tcs.TrySetException(e.Error);
                 }
                 else  if  (e.Cancelled)
                 {
                     // 试图将基础Tasks.Task<TResult>转换为Tasks.TaskStatus.Canceled状态
                     tcs.TrySetCanceled();
                 }
                 else
                 {
                     // 试图将基础Tasks.Task<TResult>转换为TaskStatus.RanToCompletion状态。
                     tcs.TrySetResult(e.Result);
                 }
             };
             // 当Task完成时继续下面的Task,显示Task的状态
             // 为了让下面的任务在GUI线程上执行,必须标记为TaskContinuationOptions.ExecuteSynchronously
             // 如果没有这个标记,任务代码会在一个线程池线程上运行
             tcs.Task.ContinueWith(t =>
             {
                 if  (t.IsCanceled)
                 {
                     Console.WriteLine( "操作已被取消" );
                 }
                 else  if  (t.IsFaulted)
                 {
                     Console.WriteLine( "异常发生,异常信息为:"  + t.Exception.GetBaseException().Message);
                 }
                 else
                 {
                     Console.WriteLine(String.Format( "操作已完成,结果为:{0}" , t.Result));
                 }
             }, TaskContinuationOptions.ExecuteSynchronously);
             // 开始异步操作
             webClient.DownloadStringAsync( new  Uri( "http://msdn.microsoft.com/zh-CN/" ));
             #endregion

五、小结

  本专题关于TAP的内容就介绍到这里了,本专题主要以实现一个文件下载程序要讲述基于任务的异步模式所带来的简便,这个也是.NET 4.0中提出TAP的原因所在吧,最后介绍了TAP与APM和EAP模式之间的转化,通过这部分大家可以清楚知道以前的异步实现如何向新的异步模式的迁移,以及从他们的转换实现代码中也可以比较他们之间的不同。然而在.NET 4.5中,微软对异步编程又做了更好的支持——提供了async和await两个关键字,这两个关键字使我们异步编程如同步编程一样的简单,彻底改变了实现异步编程所面临的委托回调,跨线程访问控件等问题,具体这部分内容,我将在下个专题中为大家介绍。