zoukankan      html  css  js  c++  java
  • 【温故知新】C#基于事件的异步模式(EAP)

    在开发winform和调用asp.net的web service引用的时候,会出现许多命名为 MethodNameAsync 的方法。

    例如:

    winform的按钮点击

    this.button1.Click += new System.EventHandler(this.button1_Click);
    
    private void button1_Click(object sender, EventArgs e) {   //dosomething }

    这就是基于事件的异步编程模式,它实现了不影响主线程的情况下异步调用耗时方法,在完成的时候通过事件进行函数回调,一般情况下,我们都应该使用该模式来公开类的异步方法。

    那什么时候需要使用IAsyncResult 模式呢?微软给出了很好的答案,见https://msdn.microsoft.com/zh-cn/library/ms228966(v=vs.110).aspx

    接下来就让我们通过代码实现一个基于事件的异步模式

    代码场景

    我们模拟一个下载器,下载我喜爱的影片,过程中实时展示下载进度,并且在下载完成后进行提醒。

    核心代码如下:

        public class Downloader
        {
    
            //声明事件参数
            public class DownloadCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs
            {
                private string m_result;
                public DownloadCompletedEventArgs(string result, Exception error, bool cancelled, Object userState)
                    : base(error, cancelled, userState)
                {
                    m_result = result;
                }
                public string Result
                {
                    get
                    {
                        //只读属性在返回属性值之前应调用 RaiseExceptionIfNecessary 方法。 如果组件的异步辅助代码将某一异常指定给 Error 属性或将 Cancelled 属性设置为 true,则该属性将在客户端尝试读取它的值时引发异常。 这会防止客户端因异步操作失败而访问可能无效的属性。
                        RaiseExceptionIfNecessary();
                        return m_result;
                    }
                }
            }
    
            //声明委托
            public delegate void ProgressChangedEventHandler(
        ProgressChangedEventArgs e);//ProgressChangedEventArgs自带有了。
    
            public delegate void DownloadCompletedEventHandler(object sender,
        DownloadCompletedEventArgs e);
    
            //内部下载处理委托
            private delegate string DownLoadHandler(string url, string name, AsyncOperation asyncOp);
    
            //声明事件
            public event ProgressChangedEventHandler ProgressChanged;
            public event DownloadCompletedEventHandler DownloadCompleted;
    
            //声明SendOrPostCallback委托,通过AsyncOperation.post会将这些调用正确地封送到应用程序模型的合适线程或上下文。
            private SendOrPostCallback onProgressChangedDelegate;
            private SendOrPostCallback onDownloadCompletedDelegate;
    
            /// <summary>
            /// 构造函数
            /// </summary>
            public Downloader()
            {
                onProgressChangedDelegate = new SendOrPostCallback(onProgressChanged);
                onDownloadCompletedDelegate = new SendOrPostCallback(onDownloadComplete);
            }
    
            /// <summary>
            /// 通过AsyncOperation调用onProgressChangedDelegate委托关联该函数,保证运行在合适线程
            /// </summary>
            /// <param name="state"></param>
            private void onProgressChanged(object state)
            {
                if (ProgressChanged != null)
                {
                    ProgressChangedEventArgs e =
                        state as ProgressChangedEventArgs;
                    ProgressChanged(e);
                }
            }
    
            private void onDownloadComplete(object state)
            {
                if (DownloadCompleted != null)
                {
                    DownloadCompletedEventArgs e =
                        state as DownloadCompletedEventArgs;
                    DownloadCompleted(this, e);
                }
            }
    
            /// <summary>
            /// 异步下载文件
            /// </summary>
            /// <param name="url"></param>
            /// <param name="name"></param>
            public void DownloadAsync(string url, string name)
            {
    
                //url不能为null
                if (url == null)
                {
                    throw new ArgumentNullException("url");
                }
    
                //userSuppliedState 参数来唯一地标识每个调用,以便区分执行异步操作的过程中所引发的事件。
                //不唯一的任务 ID 可能会导致您的实现无法正确报告进度和其他事件。 代码中应检查是否存在不唯一的任务 ID,并且在检测到不唯一的任务 ID 时引发 SystemArgumentException。
                //由于我们不用监控异步操作状态,所以参数设为null
                AsyncOperation asyncOp = AsyncOperationManager.CreateOperation(null);
    
                //异步委托调用download,如果不想再声明DownLoadHandler委托,用Action或Fun代替也行。
                DownLoadHandler dh = new DownLoadHandler(DownLoad);
                dh.BeginInvoke("http://pan.baidu.com/movie.avi", "乔布斯传", asyncOp, new AsyncCallback(DownloadCallBack), asyncOp);
            }
    
            private void DownloadCallBack(IAsyncResult iar)
            {
                AsyncResult aresult = (AsyncResult)iar;
                DownLoadHandler dh = aresult.AsyncDelegate as DownLoadHandler;
                string r = dh.EndInvoke(iar);
                AsyncOperation ao = iar.AsyncState as AsyncOperation;
                //特定任务调用此方法后,再调用其相应的 AsyncOperation 对象会引发异常。
                ao.PostOperationCompleted(onDownloadCompletedDelegate, new DownloadCompletedEventArgs(r, null, false, null));
            }
    
            /// <summary>
            /// 提供给外部调用的同步方法
            /// </summary>
            /// <param name="url"></param>
            /// <param name="name"></param>
            /// <returns></returns>
            public string DownLoad(string url, string name)
            {
                return DownLoad(url, name, null);
            }
    
    
            private string DownLoad(string url, string name, AsyncOperation asyncOp)
            {
                //url不能为null
                if (url == null)
                {
                    throw new ArgumentNullException("url");
                }
                for (int i = 0; i < 10; i++)
                {
                    int p = i * 10;
                    Debug.WriteLine("执行线程:" + Thread.CurrentThread.ManagedThreadId + ",传输进度:" + p + "%");
                    Thread.Sleep(1000);
                    //不为空则是异步
                    if (asyncOp != null)
                    {
                        //在适合于应用程序模型的线程或上下文中调用委托。
                        asyncOp.Post(onProgressChangedDelegate, new ProgressChangedEventArgs(p, null));
                    }
                }
                return name + "文件下载完成!";
            }
    
        }

    在客户端调用:

    private async void button1_Click_1(object sender, EventArgs e)
            {
                Downloader downloader = new Downloader();
                downloader.DownloadCompleted += downloader_DownloadCompleted;
                downloader.ProgressChanged += downloader_ProgressChanged;
                Debug.WriteLine("调用线程:" + Thread.CurrentThread.ManagedThreadId);
    
                //异步调用
                downloader.DownloadAsync("http://baidu.com", "乔布斯传.avi");
    
                //同步调用,UI线程卡死
                //string r = downloader.DownLoad("http://baidu.com", "乔布斯传.avi");
                //textBox1.AppendText(r);
            }
    
            void downloader_ProgressChanged(ProgressChangedEventArgs e)
            {
                textBox1.AppendText(("事件回调线程:" + Thread.CurrentThread.ManagedThreadId + "下载了" + e.ProgressPercentage + "%
    "));
            }
    
            void downloader_DownloadCompleted(object sender, Downloader.DownloadCompletedEventArgs e)
            {
                textBox1.AppendText(("事件回调线程:" + Thread.CurrentThread.ManagedThreadId + "下载完成,文件为" + e.Result + "
    "));
            }

    运行结果:

    调用线程:9
    执行线程:10,传输进度:0%
    执行线程:10,传输进度:10%
    执行线程:10,传输进度:20%
    执行线程:10,传输进度:30%
    执行线程:10,传输进度:40%
    执行线程:10,传输进度:50%
    执行线程:10,传输进度:60%
    执行线程:10,传输进度:70%
    执行线程:10,传输进度:80%
    执行线程:10,传输进度:90%

    总结:

    1、我们通过EAP模式,实现了不影响UI情况下的异步调用,归功于AsyncOperation,避免了Control.Invoke

    2、如果不在应用程序模型(包括 ASP.NET、控制台应用程序和 Windows 窗体应用程序)下正常运行,可以免去AsyncOperation这个步骤,直接在callback通知相应event

    3、实现一个这样的类有些麻烦,如果还需要监视状态,需要一个数组维护AsyncOperation

    4、微软也提供了BackgroundWorker类来执行耗时的后台操作。

    5、正因为麻烦,所以.net4.0后面又推出了Task,.net4.5中更加简化,各种封装,瞬间异步,如果使用Task,我们的异步函数将变成如下几句话。

            public async void DownloadTaskAsync(string url, string name)
            {
                AsyncOperation asyncOp = AsyncOperationManager.CreateOperation(null);
                string r = await Task<string>.Run(() =>
                {
                    return DownLoad(url, name, asyncOp);
                });
                if (DownloadCompleted != null)
                {
                    DownloadCompletedEventArgs e = new DownloadCompletedEventArgs(r, null, false, null);
                    DownloadCompleted(this, e);
                }
            }

    调用方式不变。

    实现基于事件的异步模式的最佳做法,见https://msdn.microsoft.com/zh-cn/library/ms228969(v=vs.110).aspx

    参考:

    https://msdn.microsoft.com/zh-cn/library/ms228969(v=vs.110).aspx

    https://msdn.microsoft.com/zh-cn/library/e7a34yad(v=vs.110).aspx

    补充:
    1、CreateOperation一定要在主线程调用,会自动设置上下文,否则上下文就会是另一个线程的。

    2、这里AsyncOperation的主要作用其实就是将相应委托post到CreateOperation时的上下文执行,所以DownLoadHandler中传入asyncOp参数。BeginInvoke相当于new Thread执行,尾部参数也传入asyncOp,在callback的时候post结果。DownLoad也传入asyncOp参数,post进度。

    3、对于一些简单需求,回调函数调用UI时直接判断InvokeRequired通过主线程Invoke也行。

    4、如果不需要监控异步操作状态,并且需求简单,那么可以在初始化的时候直接在UI主线程声明一个全局AsyncOperation,这样统一调用者一个对象post即可。

    DEMO下载地址:

    链接:http://pan.baidu.com/s/1qYrb81Q 密码:xu6s

  • 相关阅读:
    20200924-4 代码规范,结对要求
    20200924-2 功能测试
    20200917-1 每周例行报告
    20200917-3 白名单
    20200917-2 词频统计 已更新附加题!
    20200910-1 每周例行报告
    20200924-2功能测试
    20200924-1每周例行报告
    20200924-3单元测试
    20200924-5 四则运算,结对
  • 原文地址:https://www.cnblogs.com/leestar54/p/4591792.html
Copyright © 2011-2022 走看看