C#异步模式
2021-07-20 08:06 阿诚de窝 阅读(521) 评论(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 的方法。