.NET异步编程模式(二)

  • .NET异步编程模式(二)已关闭评论
  • 183 次浏览
  • A+
所属分类:.NET技术
摘要

在 C#1 的时候就包含了APM,在 APM 模型中,异步操作通过 IAsyncResult 接口实现,包括两个方法 BeginOperationName 和 EndOperationName ,分别表示开始和结束异步操作。

在 C#1 的时候就包含了APM,在 APM 模型中,异步操作通过 IAsyncResult 接口实现,包括两个方法 BeginOperationName 和 EndOperationName ,分别表示开始和结束异步操作。

Demo

我们先来看一个同步示例。新建WPF程序,在界面上放一个按钮。点击按钮访问外网,会有一定时间的阻塞。

private void SyncBtn_Click(object sender, RoutedEventArgs e) {     // 记录时间     Debug.WriteLine(DateTime.Now.TimeOfDay.ToString() +                      ",ThreadID = " + Thread.CurrentThread.ManagedThreadId);      // 访问外网网站网站     var req = WebRequest.Create("https://docs.newrelic.com/docs/apm/agents/net-agent/getting-started/net-agent-compatibility-requirements-net-framework/");     req.GetResponse();      // 记录时间     Debug.WriteLine(DateTime.Now.TimeOfDay.ToString() +                     ",ThreadID = " + Thread.CurrentThread.ManagedThreadId); } 

当我们点击按钮后,因为web请求是同步的,会阻塞UI线程一定时间。从输出日志上看阻塞时间是 1 秒钟左右,此时界面呈卡死状态。

.NET异步编程模式(二)

日志输出如下:

13:16:09.5031834,ThreadID = 1  13:16:10.5220362,ThreadID = 1 

从运行效果和日志,我们可以看出:

  • WebRequest方法调用前后都是在同一个线程上执行-UI线程
  • WebReqeust方法阻塞了UI线程,导致“假死”现象

WebRequest也提供了异步方法,BeginGetResponse,EndGetResponse。我们修改一下代码,新增一个按钮。

private void APM_Btn_Click(object sender, RoutedEventArgs e) {     // 记录时间     Debug.WriteLine("1-" + DateTime.Now.TimeOfDay.ToString() +                     ",ThreadID = " + Thread.CurrentThread.ManagedThreadId);      // 访问外网网站网站     var req = WebRequest.Create("https://docs.newrelic.com/docs/apm/agents/net-agent/getting-started/net-agent-compatibility-requirements-net-framework/");     req.BeginGetResponse(new AsyncCallback(t => { WebRequestCallback(t,req); }), null);      // 记录时间     Debug.WriteLine("3-" + DateTime.Now.TimeOfDay.ToString() +                     ",ThreadID = " + Thread.CurrentThread.ManagedThreadId); }  /// <summary> /// 异步回调 /// </summary> /// <param name="result"></param> private void WebRequestCallback(IAsyncResult result, WebRequest request) {     var response = request.EndGetResponse(result);     // 获取返回数据流     var stream = response.GetResponseStream();      using(StreamReader reader = new StreamReader(stream))     {         StringBuilder sb = new StringBuilder();         while(!reader.EndOfStream)         {             var content = reader.ReadLine();             sb.Append(content);         }          // 记录时间         Debug.WriteLine("2-" + DateTime.Now.TimeOfDay.ToString() +                         ",ThreadID = " + Thread.CurrentThread.ManagedThreadId);     } } 

运行效果如下:

.NET异步编程模式(二)

日志输出如下:

1-13:10:01.7734197,ThreadID = 1  3-13:10:01.8826176,ThreadID = 1  2-13:10:03.2614022,ThreadID = 14 

从运行效果和日志,我们可以看出:

  • 异步方法不会阻塞调用方法,调用后立刻返回
  • 异步方法会在另外一个线程上执行

IAsyncResult

BeginOperationName 方法会返回一个实现了 IAsyncResult 接口的对象。该对象存储了关于异步操作的信息。

.NET异步编程模式(二)

转到定义,我们可以看到接口中都包含哪些内容:

.NET异步编程模式(二)

自定义异步方法

实现该接口,定义自己的异步方法。

public class MyWebRequestResult : IAsyncResult {     /// <summary>     /// 用户定义属性,可以存放数据     /// </summary>     public object? AsyncState => throw new NotImplementedException();     /// <summary>     /// 获取用于等待异步操作完成的 WaitHandle     /// </summary>     public WaitHandle AsyncWaitHandle => throw new NotImplementedException();     /// <summary>     /// 表示异步操作是否是同步完成     /// </summary>     public bool CompletedSynchronously => throw new NotImplementedException();     /// <summary>     /// 表示异步操作是否完成     /// </summary>     public bool IsCompleted => throw new NotImplementedException(); } 

我们需要新建一个回调函数:

public class MyWebRequestResult : IAsyncResult {     /// <summary>     /// 异步回调函数     /// </summary>     private AsyncCallback _callback;     public string Result { get; private set; }     // 构造函数     public MyWebRequest(AsyncCallback asyncCallback, object state)     {         _callback = asyncCallback;     }     // 设置结果     public void SetComplete()     {         AsyncState = result;         Result = result;         if(null != _callback)         {             _callback(this);         }     }     // ... } 

在次之后就可以自定义 APM 异步模型了:

public IAsyncResult BeginMyWebRequest(AsyncCallback callback) {     // 1. 先给 IAsyncResult 进行赋值     var myResult = new MyWebRequestResult(callback, null);     var request = WebRequest.Create("https://docs.newrelic.com/docs/apm/agents/net-agent/getting-started/net-agent-compatibility-requirements-net-framework/");     // 2. 新建线程,执行耗时任务     new Thread(() => {         using (StreamReader sr = new StreamReader(request.GetResponse().GetResponseStream()))         {             var str = sr.ReadToEnd();             // 3. 耗时任务结束后 调用回调函数 & 保存结果             myResult.SetComplete(str);         }     }).Start();      return myResult; }  public string EndMyWebRequest(IAsyncResult asyncResult) {     MyWebRequestResult myResult = asyncResult as MyWebRequestResult;     return myResult.Result; } 

新增一个按钮,进行调用:

private void MyAPM_Btn_Click(object sender, RoutedEventArgs e) {     // 记录时间     Debug.WriteLine("1-" + DateTime.Now.TimeOfDay.ToString() +                     ",ThreadID = " + Thread.CurrentThread.ManagedThreadId);      // 调用 Begin 方法     BeginMyWebRequest(new AsyncCallback(MyAPM_Callback));      // 记录时间     Debug.WriteLine("3-" + DateTime.Now.TimeOfDay.ToString() +                     ",ThreadID = " + Thread.CurrentThread.ManagedThreadId); }  private void MyAPM_Callback(IAsyncResult result) {     // 从这里可以获得 异步操作的结果     var myResult = result as MyWebRequestResult;     var msg = EndMyWebRequest(myResult);      // 记录时间     Debug.WriteLine("2-" + DateTime.Now.TimeOfDay.ToString() +                     ",ThreadID = " + Thread.CurrentThread.ManagedThreadId); } 

运行效果如下:

.NET异步编程模式(二)

日志输出如下:

1-14:48:42.7278184,ThreadID = 1  3-14:48:42.7311174,ThreadID = 1  2-14:48:45.1049069,ThreadID = 6 

结合效果和日志,我们可以得出如下结论:

  • 自定义的异步方法没有导致 UI 卡顿
  • APM就是把耗时的任务交给新线程去做,然后利用委托进行回调

普通方法的异步

如果是普通方法,也可以通过 委托异步(BeginInvoke, EndInvoke):

public void MyAction() {     var func = new Func<string, string>(t => {         Thread.Sleep(2000);         return t;     });      func.BeginInvoke("inputStr", t => {         string result = func.EndInvoke(t);     },null); } 

总结

  1. APM 模型是基于IAsyncResult来实现异步操作的
  2. 异步操作开始时,把委托传递给 IAsyncResult
  3. 在新线程上执行耗时操作
  4. 耗时操作结束后,修改 IAsyncResult 里的结果数据,并调用 IAsyncResult 里的委托回调
  5. 在回调里获取 异步操作 的结果