天天看點

在.Net中使用異步

在寫程式的過程中,我們可能會需要對某些功能實作異步操作,比如記錄調用日志等。

提到異步,我們最容易想到的就是多線程:我們可以啟動另外一個線程,把一部分工作交給另外一個線程去執行,而目前線程繼續去做一些更加急迫的事情。這裡的“把一部分工作交給另外一個線程取執行”,是通過将要執行的函數的函數入口位址告訴另外一個線程來實作的,當新的線程有了函數的入口位址,就可以調用該函數。

我們先來看一下怎樣使用C#中的Thread類來實作異步。

使用Thread類異步執行一個方法

在C#中,Thread類是常用的用來啟動線程的類:

        static void Main(string[] args)

        {

            Thread thread = new Thread(new ThreadStart(myStartingMethod));

            thread.Start();

        }

        static void myStartingMethod()

實際上,這裡建立的ThreadStart對象,封裝的就是方法“myStartingMethod”的入口位址。C#中通過Delegate對象,可以友善的封裝函數入口位址。

而Delegate,實際上是用來描述函數定義的,比如上面提到的ThreadStart委托,他的聲明如下:

public delegate void ThreadStart();

這句話聲明了一個叫做ThreadStart的委托類型,而且該聲明表示:ThredStart這個委托類型,隻能封裝“傳回值為void、沒有參數”的函數的入口位址。如果我們給ThreadStart類的構造函數傳遞的方法不符合,則會出錯:

            // 錯誤 “myStartingMethod”的重載均與委托“System.Threading.ThreadStart”不比對

        static void myStartingMethod(int a)

實際上,我們在使用多線程時,要異步執行的函數往往會有一些參數。比如記錄日志時,我們需要告訴另外一個線程日志的資訊。

異步執行一個帶參數的方法

是以,Thread類除了接受ThreadStart委托,還接受另外一個帶參數的委托類型ParameterizedThreadStart:

            Thread thread = new Thread(new ParameterizedThreadStart(myStartingMethod));

            thread.Start(null);

        static void myStartingMethod(object threadData)

            // do something.

ParameterizedThreadStart 委托可以用來封裝傳回值為void、具有一個object類型參數的函數。這樣,我們就可以往另外一個函數中傳遞參數了——隻不過,如果要傳遞多個參數,我們必須将參數封裝一下,弄到一個object對象中去。比如下面的例子中,本來我們需要傳遞兩個整數的,但為了符合ParameterizedThreadStart的聲明,我們需要改造一下函數:

            MyStartingMethodParameterWarpper param = new MyStartingMethodParameterWarpper();

            param.X = 1;

            param.Y = 2;

            thread.Start(param);

            MyStartingMethodParameterWarpper param = (MyStartingMethodParameterWarpper)threadData;

            int value = param.X + param.Y;

            // do something

        public class MyStartingMethodParameterWarpper

            public int X;

            public int Y;

ParameterizedThreadStart委托必須與Thread.Start(Object) 方法一起使用——委托隻是用來傳遞函數入口,但函數的參數是通過Thread.Start方法傳遞的。

另外需要注意的,從這裡我們可以看到,這樣的使用方法并不是類型安全的,我們無法保證myStartingMethod方法的參數threadData永遠都是MyStartingMethodParameterWarpper 類型,是以我們還需要加上判斷;另外這樣實際上也加大了程式間的溝通成本:如果有人需要異步執行myStartingMethod方法,那麼他就必須知道其參數的實際類型并保證參數傳遞正确,而這塊編譯器已經無法通過編譯錯誤的方式通知你了。

怎樣獲得異步執行的結果?

至此,我們隻解決了傳遞參數的問題。

Thread類無法執行一個包含有傳回值的函數。我們知道“int a = Math.Sum(1, 2)”是将Sum函數的傳回結果複制給了變量a,但如果用了多線程,那麼這個線程不知道将這個傳回結果複制到哪裡,是以接受這樣的一個函數是沒有意義的。于是産生了另外一個重要的問題:如果我想要知道一步執行的結果,也就是如果我的線程函數具有傳回值,我應該怎樣做呢?

解決的方法有很多種。

順着剛才解決傳遞參數的思路,我們可能會想到:如果Thread類接受一個包含有一個object類型的輸入參數和一個object類型的輸出參數,不就可以了麼?嗯,這個思路聽起來不錯。不過很不幸的是,MS并沒有提供這個接口。

如此看來,我們是沒法直接得到異步函數的執行結果了。

不過沒關系,我們可以間接的得到——我們可以線上程函數内,把函數的傳回值儲存在一個約定好的地方,然後在主線程到那裡去取就可以了!

是以,考慮到object對象是引用類型,我們可以傳回值直接放線上程函數的參數中:        static void Main(string[] args)

            while(thread.ThreadState != ThreadState.Stopped)

            {

                Thread.Sleep(10);

            }

            Console.WriteLine(param.Value);

            param.Value = param.X + param.Y;

            public int Value;

回顧上面的封裝函數參數、封裝函數傳回值的做法,我們的思路實際上是“将線程函數的參數、傳回值封裝在對象中”。而剛剛我們也提到了,ParameterizedThreadStart 委托和 Thread.Start(Object) 方法重載使得将資料傳遞給線程過程變得簡單,但由于可以将任何對象傳遞給 Thread.Start(Object),是以這種方法并不是類型安全的。将資料傳遞給線程過程的一個更可靠的方法是将線程過程和資料字段都放入輔助對象:

            MyClass obj = new MyClass();

            obj.X = 1;

            obj.Y = 2;

            Thread thread = new Thread(new ThreadStart(obj.myStartingMethod));

            Console.WriteLine(obj.Value);

        public class MyClass

            public void myStartingMethod()

            ...{

                this.Value = this.X + this.Y;

怎樣知道線程函數已經執行完畢

剛才在我們擷取函數傳回值時,都使用了一個While循環來等待線程函數執行完畢。但這種方式可能是不好的——假設我們啟動一個線程,這個線程嘗試去獲得一個打開的資料庫連結,而主程式需要在獲得該連接配接後馬上得到通知。看下面這段:

        ...{

            Thread thread = new Thread(new ThreadStart(obj.MyStartingMethod));

            //

            if(!SomethingDone && thread.ThreadState == ThreadState.Stopped)

                DoSomething();

            // 事情1

            // .....

            // 事情2

            // 事情3

            // 事情4

            // ......

        static bool SomethingDone = false;

        static void DoSomething()

            SomethingDone = true;

            public OdbcConnection OpenConnection;

            public void MyStartingMethod()

                this.OpenConnection = new OdbcConnection();

                // do something

                this.OpenConnection.Open();

上面的代碼,雖然我們在每次執行一個代碼段後就判斷線程有沒有執行完,但實際上仍然不是及時的——仍然無法保證在函數執行完後就第一時間就啟動了函數DoSomething,因為每個代碼段執行過程中也許消耗了很長時間,而在這段時間内另一個線程早就執行完了。

這樣的主動輪詢的方法,實在是比較累,而且及時性也不好。

那麼,Thread類接受了一個函數入口位址,線程在啟動後就會去執行這個函數。那麼,假設我們給線程多傳遞一個函數入口位址,叫線程在執行完線程函數之後就馬上執行這個函數,那我們豈不是。。。就能第一時間得知函數已經執行完了?想法很好。看我們來改造:

            obj.OnMyStartingMethodCompleted = new MyStartingMethodCompleteCallback(WriteResult);

            // wait for process exit

        static void WriteResult(MyClass sender)

            Console.WriteLine(sender.Value);

        public delegate void MyStartingMethodCompleteCallback(MyClass sender);

            public MyStartingMethodCompleteCallback OnMyStartingMethodCompleted;

                // 函數已經執行完了,調用另外一個函數。

                this.OnMyStartingMethodCompleted(this);

注意線程方法MyStartingMethod的最後一句,這裡實際上就是執行了委托對象OnMyStartingMethodCompleted中所封裝的那個函數入。當然為此我們專門定義了一個表示方法MyStartingMethod已經執行完畢的一個委托MyStartingMethodCompleteCallback,他沒有傳回值,隻有一個參數就是方法MyStartingMethod所屬的對象。

當然,這裡的通知,我們也可以使用Event來實作。不過event的實作方法偶就不寫了,,今天寫的好累。剩下的事情,就留給大家自己搞吧。

下篇:

在上一篇文章中,我們探讨了使用Thread類實作異步的方法。

在整個過程中,可以發現Delegate這個東西出現了很多次。而仔細研究Delegate,我們發現每一個Delegate類型都自動産生了Invoke、BeginInvoke、EndInvoke等方法。而BeginInvoke、EndInvoke這兩個方法,我們馬上就可以猜到這是用來實作異步的~~

那麼我們現在就看一下怎樣使用委托來實作異步。

Delegate的BeginInvoke、EndInvoke兩個方法,是編譯器自動生成的,專門用來實作異步,這裡是MSDN中關于這兩個方法的說明:

異步委托提供以異步方式調用同步方法的能力。當同步調用一個委托時,“Invoke”方法直接對目前線程調用目标方法。如果編譯器支援異步委托,則它将生成“Invoke”方法以及“BeginInvoke”和“EndInvoke”方法。如果調用“BeginInvoke”方法,則公共語言運作庫 (CLR) 将對請求進行排隊并立即傳回到調用方。将對來自線程池的線程調用該目标方法。送出請求的原始線程自由地繼續與目标方法并行執行,該目标方法是對線程池線程運作的。如果在對“BeginInvoke”方法的調用中指定了回調方法,則當目标方法傳回時将調用該回調方法。在回調方法中,“EndInvoke”方法擷取傳回值和所有輸入/輸出參數。如果在調用“BeginInvoke”時未指定任何回調方法,則可以從調用“BeginInvoke”的線程中調用“EndInvoke”。

其中,BeginInvoke用來啟動異步,與Thread類不同的是這裡的異步使用CLR管理的。BeginInvoke方法的最後兩個參數總是一個AsyncCallback委托對象和一個object類型,其中AsyncCallback委托就是當異步執行完成時将要被調用的函數入口,也就是上一篇中用來實作“在異步完成時通知我”這個功能的。而最後一個object類型,則是用來傳遞參數的,其實與上一篇中ParameterizedThreadStart委托的參數是類似的——不過他們還是有着明顯的差別:使用ParameterizedThreadStart委托時永遠隻能接受一個object類型的參數,是以如果原本要異步執行的函數具有多個參數,必須進行封裝;而使用BeginInvoke方法則不同,編譯器生成的BeginInvoke方法前面幾個參數(除了最後兩個)的類型跟聲明委托時的參數個數和類型完全相同,這樣就不必再封裝參數了,最後一個object參數隻是一個補充的參數,一般情況下是不需要的:        

private void DoMain(string cmd, string[] args)

            SumDelegate handle = new SumDelegate(this.Sum);

            IAsyncResult ar = handle.BeginInvoke(1, 2, null, null);

        public delegate int SumDelegate(int x, int y);

        public int Sum(int x, int y)

            return x + y;

我們可以看到,在調用BeginInvoke的時候,方法的後面兩個參數就是對應的AsyncCallback和object參數,這裡因為我們沒有用到這個回調和參數,就都傳遞了null;而BeginInvoke的前面兩個方法,就對應的是Sum函數的兩個參數x和y。是以,這個BeginInvoke方法還在代碼編譯的時候就幫我們檢查了函數的輸入參數個數以及類型。

當使用Thread類時,我們可以通過判斷Thread類的ThreadStatus來判斷線程是否已經執行結束。而如果用Delegate.BeginInvoke方法,我們則需要根據其傳回的一個IAsyncResult對象的IsCompleted屬性來擷取“異步操作是否已完成的訓示”:當這個屬性變成True時,就表示異步已經執行結束:

        private void DoMain(string cmd, string[] args)

            while(!ar.IsCompleted)

            // 異步已經執行完畢

當然,前面我們提到,BeginInvoke方法總是會接收一個AsyncCallback類型的委托,當異步執行完畢後,CLR就會自動調用這個委托封裝的函數。是以,我們還可以通過這個委托來接受異步已經完成的通知:         private void DoMain(string cmd, string[] args)

            AsyncCallback callback = new AsyncCallback(this.OnSumCompleted);

            IAsyncResult ar = handle.BeginInvoke(1, 2, callback, null);

        public void OnSumCompleted(IAsyncResult ar)

            Debug.Assert(ar.IsCompleted);

注意這裡,當向BeginInvoke傳入的AsyncCallback被執行時,IAsyncResult對象的IsCompleted屬性一定是True。另外,BeginInvoke方法傳遞的最後一個object參數,實際上就是儲存在了IAsyncResult的AsyncState屬性中。

上面已經提到了兩種等待異步調用執行完畢的方法:主動輪詢 和 異步執行完畢時執行回調方法。除了這兩種方法,我們還可以通過EndInvoke方法來直接阻塞線程(并不是每次都會阻塞,這個我們下面再講)直到異步執行完成:         private void DoMain(string cmd, string[] args)

            int value = handle.EndInvoke(ar);

            Debug.Assert(value == 3);

當調用EndInvoke時,必須把BeginInvoke傳回的IAsyncResult對象作為參數傳遞,這樣EndInvoke才可以通過IAsyncResult對象得知要等待哪個方法異步執行完畢。因為在BeginInvoke傳回的IAsyncResult中,屬性AsyncWaitHandle訓示了用于等待異步執行完畢的一個句柄。如果你調用了很多次BeginInvoke,就會啟動很多個異步任務,每次調用傳回的IAsyncResult就會對應的儲存了不同的句柄。另外,這裡可以看到,EndInvoke方法的傳回結果,實際上就是我們在定義SumDelegate委托時聲明的傳回值類型,這個也是編譯器自動幫我們生成的。

那麼,我們剛才提到EndInvoke方法“并不是每次都會阻塞”。為什麼呢?原因很簡單:在EndInvoke方法内部,首先會判斷IAsyncResult.IsCompleted屬性,如果為True,則直接傳回執行結果,否則調用AsyncWaitHandle這個句柄的WaitOne方法,這個方法“阻止目前線程,直到目前的 WaitHandle 收到異步調用已經結束的信号”,然後傳回執行結果。

是以,與之對應的,我們還有另外一個方法來等待異步執行結束,那就是我們直接通路AsyncWaitHandle:

            if(!ar.IsCompleted)

                ar.AsyncWaitHandle.WaitOne();

            // 異步調用已結束。

實際上,這個方式跟EndInvoke是完全相同的。

這下我們應該明白剛才所說的“并不是每次都會阻塞”了吧?沒錯:當ar.IsCompleted為True時,就會直接傳回函數執行結果,否則才會調用WaitHandle的WaitOne來阻塞線程。

通過Delegate對象,我們可以使得我們的類更友善的支援異步方法。就好像剛才的類裡面,我們有個Sum方法,然後通過定義一個可以接受這個函數的Delegate,然後使用者就可以使用這個Delegate、AsyncCallback、IAsyncResult等對象來實作異步了。

那麼我們可不可以為客戶封裝的更簡單一點呢?就好像FileStream類,就有Read、BeginRead、EndRead三個方法,非常簡單好用。很明顯的,FileStream對象是封裝了對Delegate對象的BeginInvoke、EndInvoke方法的調用。那麼我們怎樣去實作這樣的效果呢?

下面,我們利用實作一個支援異步調用的一個類,這個類有個用于同步執行的Sum函數,和一個異步執行的BeginSum、EndSum函數:

    public class MyClass1

    {

        private delegate int SumDelegate(int a, int b);

        private SumDelegate _sumHandler;

        public MyClass1()

            this._sumHandler = new SumDelegate(this.Sum);

        public int Sum(int a, int b)

            return a + b;

        public IAsyncResult BeginSum(int a, int b, AsyncCallback callback, object stateObject)

            return this._sumHandler.BeginInvoke(a, b, callback, stateObject);

        public int EndSum(IAsyncResult asyncResult)

            return this._sumHandler.EndInvoke(asyncResult);

    }

注意這個類的内部,聲明了一個私有的委托類型“SumDelegate”,以及一個類型為SumDelegate的私有變量。我們把對這個委托的BeginInvoke、EndInvoke的調用,分别封裝在了BeginSum、EndSum中。這樣,使用者在異步調用Sum方法時,就不用為了封裝Sum函數而聲明一個新的委托了。

繼續閱讀