• C#异步模式


    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 的方法。

    天道酬勤,功不唐捐!
  • 相关阅读:
    LVS Nginx和HAproxy的区别,怎么选择最好
    PXE+kickstart自动化安装
    DHCP服务搭建
    自动化安装
    Zabbix trigger(触发器)设置
    Zabbix Agent 安装指南和 Zabbix Server 设置自动发现
    Zabbix Server安装指南
    MariaDB安装
    事件绑定
    事件驱动式
  • 原文地址:https://www.cnblogs.com/hammerc/p/14389064.html
Copyright © 2020-2023  润新知