【溫故知新】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 + "%\n"));
        }

        void downloader_DownloadCompleted(object sender, Downloader.DownloadCompletedEventArgs e)
        {
            textBox1.AppendText(("事件回調線程:" + Thread.CurrentThread.ManagedThreadId + "下載完成,文件為" + e.Result + "\n"));
        }

 

運行結果:

調用線程: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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM