淺談.Net異步編程的前世此生----APM篇

前言數據庫

在.Net程序開發過程當中,咱們常常會遇到以下場景:編程

編寫WinForm程序客戶端,須要查詢數據庫獲取數據,因而咱們根據需求寫好了代碼後,點擊查詢,發現界面卡死,沒法響應。通過調試,發現查詢數據庫這一步執行了好久,在此過程當中,UI被阻塞,沒法響應任何操做。設計模式

如何解決此問題?咱們須要分析問題成因:在WinForm窗體運行時,只有一個主線程,即爲UI線程,UI線程在此過程當中既負責渲染界面,又負責查詢數據,所以在大量耗時的操做中,UI線程沒法及時響應致使出現問題。此時咱們須要將耗時操做放入異步操做,使主線程繼續響應用戶的操做,這樣能夠大大提高用戶體驗。安全

直接編寫異步編程也許不是一件輕鬆的事,和同步編程不一樣的是,異步代碼並非始終按照寫好的步驟執行,且如何在異步執行完通知前序步驟也是其中一個問題,所以會帶來一系列的考驗。多線程

幸運的是,在.Net Framework中,提供了多種異步編程模型以及相關的API,這些模型的存在使得編寫異步程序變得容易上手。隨着Framework的不斷升級,相應的模型也在不斷改進,下面咱們一塊兒來回顧一下.Net異步編程的前世此生。異步

第一個異步編程模型:APM異步編程

概述函數

APM,全稱Asynchronous Programing Model,顧名思義,它即爲異步編程模型,最先出現於.Net Framework 1.x中。spa

它使用IAsyncResult設計模式的異步操做,通常由BeginOperationName和EndOperationName兩個方法實現,這兩個方法分別用於開始和結束異步操做,例如FileStream類中提供了BeginRead和EndRead來對文件進行異步字節讀取操做。線程

使用

在程序運行過程當中,直接調用BeginOperationName後,會將所包含的方法放入異步操做,並返回一個IAsyncResult結果,同時異步操做在另一個線程中執行。

每次在調用BeginOperationName方法後,還應調用EndOperationName方法,來獲取異步執行的結果,下面咱們一塊兒來看一個示例:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace APMTest
{
    class Program
    {
        public delegate void ConsoleDelegate();

        static void Main(string[] args)
        {
            ConsoleDelegate consoleDelegate = new ConsoleDelegate(ConsoleToUI);
            Thread.CurrentThread.Name = "主線程Thread";
            IAsyncResult ar = consoleDelegate.BeginInvoke(null, null);
            consoleDelegate.EndInvoke(ar);
            Console.WriteLine("我是同步輸出,個人名字是:" + Thread.CurrentThread.Name);
            Console.Read();
        }

        public static void ConsoleToUI()
        {
            if (Thread.CurrentThread.IsThreadPoolThread)
            {
                Thread.CurrentThread.Name = "線程池Thread";
            }
            else
            {
                Thread.CurrentThread.Name = "普通Thread";
            }
            Thread.Sleep(3000); //模擬耗時操做
            Console.WriteLine("我是異步輸出,個人名字是:" + Thread.CurrentThread.Name);
        }
    }
}

在這段示例中,咱們定義了一個委託來使用其BeginInvoke/EndInvoke方法用於咱們自定義方法的異步執行,同時將線程名稱打印出來,用於區分主線程與異步線程。

如代碼中所示,在調用BeginInvoke以後,當即調用了EndInvoke獲取結果,那麼會發生什麼呢?

以下圖所示:

看到這裏你們也許會比較詫異:爲何同步操做會在異步操做以後輸出呢?這樣不是和同步就同樣了嗎?

緣由是這樣的:EndInvoke方法會阻塞調用線程,直到異步調用結束,因爲咱們在異步操做中模擬了3s耗時操做,因此它會一直等待到3s結束後輸出異步信息,此時才完成了異步操做,進而進行下一步的同步操做。

同時在BeginInvoke返回的IAynscResult中,包含以下屬性:

經過輪詢IsCompleted屬性或使用AsyncWaitHandle屬性,都可以獲取異步操做是否完成,從而進行下一步操做,相關代碼以下所示:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace APMTest
{
    class Program
    {
        public delegate void ConsoleDelegate();

        static void Main(string[] args)
        {
            ConsoleDelegate consoleDelegate = new ConsoleDelegate(ConsoleToUI);
            Thread.CurrentThread.Name = "主線程Thread";
            IAsyncResult ar = consoleDelegate.BeginInvoke(null, null);
            //此處改成了輪詢IsCompleted屬性,AsyncWaitHandle屬性同理
            while (!ar.IsCompleted)
            {
                Console.WriteLine("等待執行...");
            }
            consoleDelegate.EndInvoke(ar);
            Console.WriteLine("我是同步輸出,個人名字是:" + Thread.CurrentThread.Name);
            Console.Read();
        }

        public static void ConsoleToUI()
        {
            if (Thread.CurrentThread.IsThreadPoolThread)
            {
                Thread.CurrentThread.Name = "線程池Thread";
            }
            else
            {
                Thread.CurrentThread.Name = "普通Thread";
            }
            Thread.Sleep(3000); //模擬耗時操做
            Console.WriteLine("我是異步輸出,個人名字是:" + Thread.CurrentThread.Name);
        }
    }
}

運行後結果以下:

能夠發現,在輪詢屬性時,程序仍然會等待異步操做完成,進而進行下一步的同步輸出,沒法達到咱們須要的效果,那麼究竟有沒有辦法解決呢?

此時咱們須要引入一個新方法:使用回調。

在以前的操做中,使用BeginInvoke方法,兩個參數總傳入的爲null。若要使用回調機制,則需傳入一個類型爲AsyncCallback的回調函數,並在最後一個參數中,傳入須要使用的參數,如如下代碼所示:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace APMTest
{
    class Program
    {
        public delegate void ConsoleDelegate();

        static void Main(string[] args)
        {
            ConsoleDelegate consoleDelegate = new ConsoleDelegate(ConsoleToUI);
            Thread.CurrentThread.Name = "主線程Thread";
            //此處傳入AsyncCallback類型的回調函數,並傳入須要使用的參數
            consoleDelegate.BeginInvoke(CallBack, consoleDelegate);
            //IAsyncResult ar = consoleDelegate.BeginInvoke(null, null);
            ////此處改成了輪詢IsCompleted屬性,AsyncWaitHandle屬性同理
            //while (!ar.IsCompleted)
            //{
            //    Console.WriteLine("等待執行...");
            //}
            //consoleDelegate.EndInvoke(ar);
            Console.WriteLine("我是同步輸出,個人名字是:" + Thread.CurrentThread.Name);
            Console.Read();
        }

        public static void ConsoleToUI()
        {
            if (Thread.CurrentThread.IsThreadPoolThread)
            {
                Thread.CurrentThread.Name = "線程池Thread";
            }
            else
            {
                Thread.CurrentThread.Name = "普通Thread";
            }
            Thread.Sleep(3000); //模擬耗時操做
            Console.WriteLine("我是異步輸出,個人名字是:" + Thread.CurrentThread.Name);
        }

        public static void CallBack(IAsyncResult ar)
        {
            //使用IAsyncResult的AsyncState獲取BeginInvoke中的參數,並用於執行EndInvoke
            ConsoleDelegate callBackDelegate = ar.AsyncState as ConsoleDelegate;
            callBackDelegate.EndInvoke(ar);
        }
    }
}

運行後結果以下:

此時能夠看出,使用回調的方式已經實現了咱們須要的效果。在同步執行時,將耗時操做放入異步操做,從而不影響同步操做的繼續執行,在異步操做完成後,回調返回相應的結果。

小結

APM模型的引入,使得編寫異步程序變的如此簡單,只需定義委託,將要執行的方法包含其中,並調用Begin/End方法對,便可實現異步編程。在一些基礎類庫中,也已經提供了異步操做的方法,直接調用便可。

同時咱們能夠看到,BeginInvoke方法,其實是調用了線程池中的線程進行操做,所以APM模型也應屬於多線程程序,同時包含主線程與線程池線程。

可是APM模型也存在一些缺點:

一、若不使用回調機制,則需等待異步操做完成後才能繼續執行,此時未達到異步操做的效果。

二、在異步操做的過程當中,沒法取消,也沒法得知操做進度。

三、若編寫GUI程序,異步操做內容與主線程未在同一線程,操做控件時會引發線程安全問題。

爲了解決這些缺陷,微軟推出了其餘的異步模式,預知後事如何,且聽下回分解。

下集預告

淺談.Net異步編程的前世此生----EAP篇

相關文章
相關標籤/搜索