• .NET异步编程模式(二)


    在 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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

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

    在这里插入图片描述

    日志输出如下:

    13:16:09.5031834,ThreadID = 1
    
    13:16:10.5220362,ThreadID = 1
    
    • 1
    • 2
    • 3

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

    • 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);
    }
    
    /// 
    /// 异步回调
    /// 
    /// 
    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);
        }
    }
    
    • 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

    运行效果如下:

    在这里插入图片描述

    日志输出如下:

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

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

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

    IAsyncResult

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

    在这里插入图片描述

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

    在这里插入图片描述

    自定义异步方法

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

    public class MyWebRequestResult : IAsyncResult
    {
        /// 
        /// 用户定义属性,可以存放数据
        /// 
        public object? AsyncState => throw new NotImplementedException();
        /// 
        /// 获取用于等待异步操作完成的 WaitHandle
        /// 
        public WaitHandle AsyncWaitHandle => throw new NotImplementedException();
        /// 
        /// 表示异步操作是否是同步完成
        /// 
        public bool CompletedSynchronously => throw new NotImplementedException();
        /// 
        /// 表示异步操作是否完成
        /// 
        public bool IsCompleted => throw new NotImplementedException();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

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

    public class MyWebRequestResult : IAsyncResult
    {
        /// 
        /// 异步回调函数
        /// 
        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);
            }
        }
        // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    在次之后就可以自定义 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

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

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    运行效果如下:

    在这里插入图片描述

    日志输出如下:

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

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

    • 自定义的异步方法没有导致 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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    总结

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

    微信公众号 - 希夏普

  • 相关阅读:
    【GDAL-java的四个常用代码示例】
    python闭包与装饰器
    【数学建模】——力学模型建立的基本理论及方法
    五、原型模式
    JAVA泛型和通配符,再也不用每次百度了
    wxPython 4.2.0 发布
    Python中的进程池及进程池锁:multiprocessing.Pool及multiprocessing.Manager().Lock()
    python 里面对于字典进行key或value排序输出
    Django中Cookie和Session的使用
    【机器学习-西瓜书】-第3章-线性回归-学习笔记-下
  • 原文地址:https://blog.csdn.net/jqwang_2009/article/details/126341192