代码改变世界

C#异步模式

2021-07-20 08:06  阿诚de窝  阅读(516)  评论(0编辑  收藏  举报

C#提供了几种针对异步代码编程的模式,我们一个一个看一下。

APM

APM即异步编程模型的简写(Asynchronous Programming Model),.NET 1.0 就开始提供的异步编程方式。

针对一些需要异步编程的同步方法,会同时提供BeginXXX和EndXXX的异步编程搭配方法,比如Socket的Connect方法是同步方法,对应的异步编程方法就是“BeginConnect”和“EndConnect”。

我们直接看看官方提供的同步和异步方法的声明:

1 // 同步方法,阻塞当前线程,连接成功后继续运行
2 public void Connect(EndPoint remoteEP)
3 // 异步方法,开新线程运行代码,不会阻塞当前线程
4 public IAsyncResult BeginConnect(EndPoint remoteEP, AsyncCallback callback, object state)
5 public void EndConnect(IAsyncResult asyncResult)

我们可以发现异步方法除了返回值变成了 IAsyncResult 对象外,还会额外增加 AsyncCallback 类型的 callback 参数,和 object 类型的 state 参数,下面我们说说这些对象的作用:

BeginXXX

开始一个异步操作,不会阻塞当前线程的执行,返回的IAsyncResult可以标识当前的异步操作对象。

EndXXX

需要传入BeginXXX返回的IAsyncResult对象,在对应的BeginXXX异步操作还没有结束时,调用该方法后会立即阻塞当前的线程的执行,并等待异步完成后,返回异步操作的结果并让线程继续执行。

注意,Socket的同步方法Connect没有返回值,所以EndConnect也没有返回值;我们看看FileStream的Read方法有一个int返回值(读入缓冲区中的总字节数。),当使用异步方法调用时,要获得这个返回值就可以通过调用EndRead方法得到了。

IAsyncResult

标记当前异步操作的唯一标识,提供当前异步操作是否完成的标志、而AsyncState属性,就是调用BeginXXX时,传入的最后一个对象。

AsyncCallback

本质是一个委托,我们看下其定义:

public delegate void AsyncCallback(IAsyncResult ar);

在异步执行完成后。会调用这个委托方法告知到主线程异步完成。

异步APM实现

除了使用官方提供的BeginXXX和EndXXX之外,我们该如何自己实现类似的异步方法呢?请往下看:

委托的异步APM

.NET除了对大量的API提供了BeginXXX和EndXXX的APM异步编程模型实现,还会对所有的委托都提供 BeginInvoke 和 EndInvoke 的方法,如同API中提供的 BeginXXX 和 EndXXX 一致,我们可以通过这个特性,快速的编写异步代码,如下:

 1 using System;
 2 using System.Diagnostics;
 3 using System.Threading;
 4 
 5 namespace NewStudyTest
 6 {
 7     public class APMDelegateTest
 8     {
 9         public APMDelegateTest()
10         {
11             Func<int, int> WaitSecond = time =>
12             {
13                 Console.Out.WriteLine("开始执行异步操作");
14 
15                 Thread.Sleep(time);
16 
17                 Console.Out.WriteLine("异步操作执行完成");
18 
19                 return new Random().Next();
20             };
21 
22             WaitSecond.BeginInvoke(3000, ar =>
23             {
24                 Console.Out.WriteLine("返回值: " + WaitSecond.EndInvoke(ar) + ", " + ar.AsyncState);
25             }, "hello APM");
26 
27             Console.Out.WriteLine("程序继续执行");
28 
29             // 因为 BeginInvoke 启动的是后台线程,所以这里要避免程序主线程关闭
30             Thread.Sleep(5000);
31         }
32     }
33 }

注意:BeginInvoke方法是从ThreadPool中取出一个线程来执行这个方法。

实现自己的BeginXXX和EndXXX

其实我们运用上面提到的委托的 BeginInvoke 和 EndInvoke 方法就可以实现自己的异步方法,如下:

 1 using System;
 2 using System.Threading;
 3 
 4 namespace NewStudyTest
 5 {
 6     public class APMTest
 7     {
 8         public static void Test()
 9         {
10             var test = new APMTest();
11             test.BeginWaitSecond(3000, ar =>
12             {
13                 Console.Out.WriteLine("返回值: " + test.EndWaitSecond(ar) + ", " + ar.AsyncState);
14             }, "hello APM");
15 
16             Console.Out.WriteLine("程序继续执行");
17 
18             // 因为 BeginInvoke 启动的是后台线程,所以这里要避免程序主线程关闭
19             Thread.Sleep(5000);
20         }
21 
22         public Func<int, int> _waitSecond;
23 
24         public APMTest()
25         {
26             _waitSecond = WaitSecond;
27         }
28 
29         public int WaitSecond(int time)
30         {
31             Thread.Sleep(time);
32             return new Random().Next();
33         }
34 
35         public IAsyncResult BeginWaitSecond(int time, AsyncCallback callback, object state)
36         {
37             return _waitSecond.BeginInvoke(time, callback, state);
38         }
39 
40         public int EndWaitSecond(IAsyncResult asyncResult)
41         {
42             return _waitSecond.EndInvoke(asyncResult);
43         }
44     }
45 }

直接调用委托对应的方法即可,是不是非常简单。

EAP

EAP 是 Event-based Asynchronous Pattern(基于事件的异步模型)的简写,在APM中不支持对异步操作的取消,也没有提供对进度报告的功能,所以在 .NET 2.0 中增加了 EAP 来处理这些问题。

基于EPA的类将具有一个或者多个以Async为后缀的方法和对应的Completed等相应的事件,并且这些类都支持异步方法的取消、进度报告和报告结果。

当我们调用实现基于事件的异步模式的类的 XxxAsync方法时,即代表开始了一个异步操作,该方法调用完之后会使一个线程池线程去执行耗时的操作。并且基于事件的异步模式是建立了APM的基础之上的。

下面我们看一个例子:

 1 using System;
 2 using System.Net;
 3 
 4 namespace NewStudyTest
 5 {
 6     public class EAPTest
 7     {
 8         public EAPTest()
 9         {
10             WebClient wc = new WebClient();
11             wc.DownloadStringCompleted += DownloadStringCompleted;
12             wc.DownloadStringAsync(new Uri("http://www.baidu.com"));
13             Console.ReadKey();
14         }
15 
16         private static void DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
17         {
18             Console.WriteLine("网页:" + e.Result);
19         }
20     }
21 }

使用上是不是更加的简洁和直观了。

实现自己的XXXAsync

  1 using System;
  2 using System.ComponentModel;
  3 using System.Runtime.Remoting.Messaging;
  4 using System.Threading;
  5 
  6 namespace NewStudyTest
  7 {
  8     /// <summary>
  9     /// 扩展 AsyncCompletedEventArgs 添加内容属性
 10     /// </summary>
 11     public class DownloadCompletedEventArgs : AsyncCompletedEventArgs
 12     {
 13         private string _content;
 14 
 15         public DownloadCompletedEventArgs(string content, Exception error, bool cancelled, Object userState) : base(error, cancelled, userState)
 16         {
 17             _content = content;
 18         }
 19 
 20         public string Content
 21         {
 22             get { return _content; }
 23         }
 24     }
 25 
 26     public class EAPDownloader
 27     {
 28         public static void Test()
 29         {
 30             var downloader = new EAPDownloader();
 31             downloader.progressChanged += (sender, args) =>
 32             {
 33                 Console.Out.WriteLine("下载中: " + args.ProgressPercentage);
 34             };
 35             downloader.downloadCompleted += (sender, args) =>
 36             {
 37                 Console.Out.WriteLine("下载完成:" + args.Content + ",是否取消:" + args.Cancelled);
 38             };
 39             downloader.DownloadAsync("http://xiazai.com/xxx.avi", "Hello");
 40 
 41             Thread.Sleep(5000);
 42             downloader.CancelAsync();
 43 
 44             Console.Out.WriteLine("程序继续执行");
 45             Console.ReadKey();
 46         }
 47 
 48         // 下载完成委托
 49         public delegate void DownloadCompletedEventHandler(object sender, DownloadCompletedEventArgs e);
 50 
 51         // 对外的事件
 52         public event ProgressChangedEventHandler progressChanged;
 53         public event DownloadCompletedEventHandler downloadCompleted;
 54 
 55         // AsyncOperation 使用的委托
 56         private SendOrPostCallback _onProgressChangedDelegate;
 57         private SendOrPostCallback _onDownloadCompletedDelegate;
 58 
 59         // 实际上实现下载模拟的代码委托, 即耗时函数
 60         private delegate string DownLoadHandler(string url, string name, AsyncOperation asyncOp);
 61 
 62         // 记录是否调用了取消异步方法
 63         private bool _cancelled = false;
 64 
 65         public EAPDownloader()
 66         {
 67             _onProgressChangedDelegate = new SendOrPostCallback(onProgressChanged);
 68             _onDownloadCompletedDelegate = new SendOrPostCallback(onDownloadComplete);
 69         }
 70 
 71         private void onProgressChanged(object state)
 72         {
 73             if (progressChanged != null)
 74             {
 75                 ProgressChangedEventArgs e = state as ProgressChangedEventArgs;
 76                 progressChanged(this, e);
 77             }
 78         }
 79 
 80         private void onDownloadComplete(object state)
 81         {
 82             if (downloadCompleted != null)
 83             {
 84                 DownloadCompletedEventArgs e = state as DownloadCompletedEventArgs;
 85                 downloadCompleted(this, e);
 86             }
 87         }
 88 
 89         public string DownLoad(string url, string name)
 90         {
 91             return RealDownLoad(url, name, null);
 92         }
 93 
 94         public void DownloadAsync(string url, string name)
 95         {
 96             // 异步操作的唯一标识对象
 97             AsyncOperation asyncOp = AsyncOperationManager.CreateOperation(null);
 98 
 99             // 这里异步的实现本质上还是使用 APM 的方式
100             DownLoadHandler dh = new DownLoadHandler(RealDownLoad);
101             dh.BeginInvoke(url, name, asyncOp, new AsyncCallback(DownloadCallBack), asyncOp);
102         }
103 
104         private void DownloadCallBack(IAsyncResult iar)
105         {
106             AsyncResult aresult = (AsyncResult)iar;
107             DownLoadHandler dh = aresult.AsyncDelegate as DownLoadHandler;
108             // 获取返回值
109             string r = dh.EndInvoke(iar);
110             AsyncOperation ao = iar.AsyncState as AsyncOperation;
111             // 抛出完成事件
112             ao.PostOperationCompleted(_onDownloadCompletedDelegate, new DownloadCompletedEventArgs(r, null, _cancelled, null));
113         }
114 
115         private string RealDownLoad(string url, string name, AsyncOperation asyncOp)
116         {
117             _cancelled = false;
118             for (int i = 0; i < 10; i++)
119             {
120                 int p = i * 10;
121                 Console.Out.WriteLine("执行线程:" + Thread.CurrentThread.ManagedThreadId + ",传输进度:" + p + "%");
122                 Thread.Sleep(1000);
123                 // 取消异步操作
124                 if (_cancelled)
125                 {
126                     return name + "文件下载取消!";
127                 }
128                 // 不为空则是异步
129                 if (asyncOp != null)
130                 {
131                     // 抛出进度事件
132                     asyncOp.Post(_onProgressChangedDelegate, new ProgressChangedEventArgs(p, null));
133                 }
134             }
135             return name + "文件下载完成!";
136         }
137 
138         public void CancelAsync()
139         {
140             _cancelled = true;
141         }
142     }
143 }

例子中,我们模拟了下载的异步操作,并实现了下载进度的通知和取消异步的操作。

实际上,我们发现,EAP的异步实现仍然使用的还是APM的委托来实现的

AsyncOperation

标记一个唯一的异步操作,可以简单的理解为异步操作的唯一ID,提供的Post和PostOperationCompleted方法可以向调用的线程发送指定的消息,其用的PostMessage发送到线程,具体发送到那个线程,要看你同步上下文是和那个线程相关的。

接收消息的地方统一使用SendOrPostCallback委托来接收即可,接收到消息再调用对应的事件即可完成事件的异步调用了。

BackgroundWorker

上面我们发现实现一个EAP的异步模型还是需要编写比较多的代码的,为了简化编写的代码量,微软为我们提供了名为BackgroundWorker的类来使用。

一些耗时较长的CPU密集型运算需要开新线程执行时,就可以方便的用该类实现EAP的异步模型。

 1 using System;
 2 using System.ComponentModel;
 3 using System.Threading;
 4 
 5 namespace NewStudyTest
 6 {
 7     public class BackgroundWorkerTest
 8     {
 9         public static void Test()
10         {
11             BackgroundWorker backgroundWorker = new BackgroundWorker();
12 
13             // 支持报告进度事件
14             backgroundWorker.WorkerReportsProgress = true;
15             // 支持异步取消
16             backgroundWorker.WorkerSupportsCancellation = true;
17 
18             backgroundWorker.ProgressChanged += (sender, args) =>
19             {
20                 Console.Out.WriteLine("执行中:" + args.ProgressPercentage + ", " + args.UserState);
21             };
22             backgroundWorker.RunWorkerCompleted += (sender, args) =>
23             {
24                 if (args.Cancelled)
25                 {
26                     Console.Out.WriteLine("执行取消!");
27                 }
28                 else
29                 {
30                     Console.Out.WriteLine("执行完毕:" + args.Result);
31                 }
32             };
33             backgroundWorker.DoWork += (sender, args) =>
34             {
35                 BackgroundWorker bw = sender as BackgroundWorker;
36 
37                 // 获取参数
38                 int count = (int)args.Argument;
39 
40                 int sum = 0;
41                 for (int i = 0; i < count; i++)
42                 {
43                     sum++;
44 
45                     // 模拟耗时操作
46                     Thread.Sleep(1000);
47 
48                     if (bw.CancellationPending)
49                     {
50                         args.Cancel = true;
51                         return;
52                     }
53 
54                     // 通知进度
55                     bw.ReportProgress(i, "msg " + i);
56                 }
57 
58                 args.Result = sum;
59             };
60             // 开始执行,可传参数
61             backgroundWorker.RunWorkerAsync(10);
62 
63             Thread.Sleep(5000);
64             backgroundWorker.CancelAsync();
65 
66             Console.Out.WriteLine("程序继续执行");
67             Console.ReadKey();
68         }
69     }
70 }

可以发现通过使用BackgroundWork类可以省去自己实现EAP的繁琐步骤,可以专注于异步的实现。

TAP

进入.NET4.0的时代,微软提出了基于任务的异步模式(Task-based Asynchronous Pattern),该模式主要使用同样在.NET4.0中引入的任务(即 System.Threading.Tasks.Task 和 Task<T> 类)来完成异步编程。

我们怎样区分.NET类库中的类实现了基于任务的异步模式呢? 这个识别方法很简单,当看到类中存在TaskAsync为后缀的方法时就代表该类实现了TAP

我们先直接看例子:

 1 using System;
 2 using System.Threading;
 3 using System.Threading.Tasks;
 4 
 5 namespace NewStudyTest
 6 {
 7     public class TAPTest
 8     {
 9         public TAPTest()
10         {
11             CancellationTokenSource cts = new CancellationTokenSource();
12 
13             Task task = new Task((() =>
14             {
15                 int result = DoCacl(10, cts.Token, new Progress<int>((i =>
16                 {
17                     Console.Out.WriteLine("执行中:" + i);
18                 })));
19                 Console.Out.WriteLine("执行完毕:" + result);
20             }));
21             task.Start();
22 
23             Thread.Sleep(5000);
24             cts.Cancel();
25 
26             Console.Out.WriteLine("程序继续执行");
27             Console.ReadKey();
28         }
29 
30         public int DoCacl(int count, CancellationToken ct, IProgress<int> progress)
31         {
32             int sum = 0;
33 
34             for (int i = 0; i < count; i++)
35             {
36                 sum++;
37 
38                 // 模拟耗时操作
39                 Thread.Sleep(1000);
40 
41                 // 取消异步的判断
42                 if (ct.IsCancellationRequested)
43                 {
44                     return -1;
45                 }
46 
47                 // 进度报告
48                 progress.Report(i);
49             }
50 
51             return sum;
52         }
53     }
54 }

我们发现通过使用Task类,异步代码的编写变得更加的简洁和易于理解,下面我们说说用到的一些类型。

CancellationTokenSource

支持取消异步操作的类。

CancellationToken

标识唯一的异步进程,当CancellationTokenSource调用取消后,可以通过判断其IsCancellationRequested来确定是否要退出耗时操作。

IProgress

向外部通知进度消息的接口。

异步方法直接返回值的例子

 1 using System;
 2 using System.Threading;
 3 using System.Threading.Tasks;
 4 
 5 namespace NewStudyTest
 6 {
 7     public class TAPTest
 8     {
 9         public TAPTest()
10         {
11             Task<int> task = new Task<int>((() =>
12             {
13                 Thread.Sleep(3000);
14 
15                 return 123;
16             }));
17             task.Start();
18 
19             Console.Out.WriteLine("程序继续执行");
20 
21             // 阻塞线程等待 Task 执行完毕
22             task.Wait();
23 
24             Console.Out.WriteLine("异步结果:" + task.Result);
25 
26             Console.ReadKey();
27         }
28     }
29 }

语法糖await和async

为了更方便的编写异步代码,.NET4.5(C#5.0)中,提供了语法糖await和async来实现用同步的方式来编写异步代码,注意需要和Task配合。

我们先看一个没有用该语法糖的例子:

 1 using System;
 2 using System.Threading;
 3 using System.Threading.Tasks;
 4 
 5 namespace NewStudyTest
 6 {
 7     public class TaskTest
 8     {
 9         public TaskTest()
10         {
11             Task.Run((() =>
12             {
13                 Task task1 = Execute1();
14                 task1.Wait();
15                 Task<int> task2 = Execute2();
16                 task2.Wait();
17                 Console.Out.WriteLine("返回值: " + task2.Result);
18                 Task task3 = Execute3();
19                 task3.Wait();
20 
21                 Console.Out.WriteLine("执行完毕");
22             }));
23 
24             Console.Out.WriteLine("程序继续执行");
25             Console.ReadKey();
26         }
27 
28         private Task Execute1()
29         {
30             Task task = new Task((() =>
31             {
32                 Thread.Sleep(1000);
33                 Console.Out.WriteLine("耗时操作1");
34             }));
35             task.Start();
36             return task;
37         }
38 
39         private Task<int> Execute2()
40         {
41             Task<int> task = new Task<int>((() =>
42             {
43                 Thread.Sleep(1000);
44                 Console.Out.WriteLine("耗时操作2");
45                 return 123;
46             }));
47             task.Start();
48             return task;
49         }
50 
51         private Task Execute3()
52         {
53             Task task = new Task((() =>
54             {
55                 Thread.Sleep(1000);
56                 Console.Out.WriteLine("耗时操作3");
57             }));
58             task.Start();
59             return task;
60         }
61     }
62 }

再看使用了await和async语法糖的例子:

 1 using System;
 2 using System.Threading;
 3 using System.Threading.Tasks;
 4 
 5 namespace NewStudyTest
 6 {
 7     public class Task2Test
 8     {
 9         public Task2Test()
10         {
11             Task.Run(async () =>
12             {
13                 await Execute1();
14                 Console.Out.WriteLine("返回值: " + await Execute2());
15                 await Execute3();
16 
17                 Console.Out.WriteLine("执行完毕");
18             });
19 
20             Console.Out.WriteLine("程序继续执行");
21             Console.ReadKey();
22         }
23 
24         private async Task Execute1()
25         {
26             Thread.Sleep(1000);
27             Console.Out.WriteLine("耗时操作1");
28         }
29 
30         private async Task<int> Execute2()
31         {
32             Thread.Sleep(1000);
33             Console.Out.WriteLine("耗时操作2");
34             return 123;
35         }
36 
37         private async Task Execute3()
38         {
39             Thread.Sleep(1000);
40             Console.Out.WriteLine("耗时操作3");
41         }
42     }
43 }

实现的效果完全一致,但是使用await和async可以更简洁也更像同步代码易于理解。

需要注意的地方

标记为 async 的异步函数的返回类型只能为: Task、Task<TResult>。

  • Task<TResult>: 代表一个返回值T类型的操作。
  • Task: 代表一个无返回值的操作。

await 只能修饰被调用的 async 的方法。