在.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類的構造函數傳遞的方法不符合,則會出錯:

 

        static void Main(string[] args)
        {
            // 錯誤 “myStartingMethod”的重載均與委託“System.Threading.ThreadStart”不匹配
            Thread thread = new Thread(new ThreadStart(myStartingMethod));
            thread.Start();
        }

        static void myStartingMethod(int a)
        {
        }

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

異步執行一個帶參數的方法
因此,Thread類除了接受ThreadStart委託,還接受另外一個帶參數的委託類型ParameterizedThreadStart:

        static void Main(string[] args)
        {
            Thread thread = new Thread(new ParameterizedThreadStart(myStartingMethod));
            thread.Start(null);
        }

        static void myStartingMethod(object threadData)
        {
            // do something.
        }

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

        static void Main(string[] args)
        {
            Thread thread = new Thread(new ParameterizedThreadStart(myStartingMethod));
            MyStartingMethodParameterWarpper param = new MyStartingMethodParameterWarpper();
            param.X = 1;
            param.Y = 2;
            thread.Start(param);
        }

        static void myStartingMethod(object threadData)
        {
            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)
        {
            Thread thread = new Thread(new ParameterizedThreadStart(myStartingMethod));
            MyStartingMethodParameterWarpper param = new MyStartingMethodParameterWarpper();
            param.X = 1;
            param.Y = 2;
            thread.Start(param);
            while(thread.ThreadState != ThreadState.Stopped)
            {
                Thread.Sleep(10);
            }
            Console.WriteLine(param.Value);
        }

        static void myStartingMethod(object threadData)
        {
            MyStartingMethodParameterWarpper param = (MyStartingMethodParameterWarpper)threadData;
            param.Value = param.X + param.Y;
        }

        public class MyStartingMethodParameterWarpper
        {
            public int X;
            public int Y;
            public int Value;
        }

 

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

 

        static void Main(string[] args)
        {
            MyClass obj = new MyClass();
            obj.X = 1;
            obj.Y = 2;
            Thread thread = new Thread(new ThreadStart(obj.myStartingMethod));
            thread.Start();
            while(thread.ThreadState != ThreadState.Stopped)
            {
                Thread.Sleep(10);
            }
            Console.WriteLine(obj.Value);
        }

        public class MyClass
        {
            public int X;
            public int Y;
            public int Value;

            public void myStartingMethod()
            ...{
                this.Value = this.X + this.Y;
            }
        }

怎樣知道線程函數已經執行完畢
剛纔在我們獲取函數返回值時,都使用了一個While循環來等待線程函數執行完畢。但這種方式可能是不好的——假設我們啓動一個線程,這個線程嘗試去獲得一個打開的數據庫鏈接,而主程序需要在獲得該連接後馬上得到通知。看下面這段:

        static void Main(string[] args)
        ...{
            MyClass obj = new MyClass();
            Thread thread = new Thread(new ThreadStart(obj.MyStartingMethod));
            thread.Start();

            //
            if(!SomethingDone && thread.ThreadState == ThreadState.Stopped)
            ...{
                DoSomething();
            }

            // 事情1
            // .....
            if(!SomethingDone && thread.ThreadState == ThreadState.Stopped)
            ...{
                DoSomething();
            }

            // 事情2
            // .....
            if(!SomethingDone && thread.ThreadState == ThreadState.Stopped)
            ...{
                DoSomething();
            }

            // 事情3
            // .....
            if(!SomethingDone && thread.ThreadState == ThreadState.Stopped)
            ...{
                DoSomething();
            }

            // 事情4
            // .....
            if(!SomethingDone && thread.ThreadState == ThreadState.Stopped)
            ...{
                DoSomething();
            }

            // ......
        }

        static bool SomethingDone = false;
        static void DoSomething()
        {
            SomethingDone = true;
            // do something
        }

        public class MyClass
        {
            public OdbcConnection OpenConnection;

            public void MyStartingMethod()
            ...{
                this.OpenConnection = new OdbcConnection();
                // do something
                this.OpenConnection.Open();
            }
        }

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

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

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

        static void Main(string[] args)
        {
            MyClass obj = new MyClass();
            obj.X = 1;
            obj.Y = 2;
            obj.OnMyStartingMethodCompleted = new MyStartingMethodCompleteCallback(WriteResult);
            Thread thread = new Thread(new ThreadStart(obj.MyStartingMethod));
            thread.Start();

            // wait for process exit
        }

        static void WriteResult(MyClass sender)
        {
            Console.WriteLine(sender.Value);
        }

        public delegate void MyStartingMethodCompleteCallback(MyClass sender);

        public class MyClass
        {
            public int X;
            public int Y;
            public int Value;
            public MyStartingMethodCompleteCallback OnMyStartingMethodCompleted;

            public void MyStartingMethod()
            {
                this.Value = this.X + this.Y;

                // 函數已經執行完了,調用另外一個函數。
                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)
        {
            SumDelegate handle = new SumDelegate(this.Sum);
            IAsyncResult ar = handle.BeginInvoke(1, 2, null, null);
            while(!ar.IsCompleted)
            {
                Thread.Sleep(10);
            }
            // 異步已經執行完畢
        }

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

        public int Sum(int x, int y)
        {
            return x + y;
        }


當然,前面我們提到,BeginInvoke方法總是會接收一個AsyncCallback類型的委託,當異步執行完畢後,CLR就會自動調用這個委託封裝的函數。因此,我們還可以通過這個委託來接受異步已經完成的通知:         private void DoMain(string cmd, string[] args)
        {
            SumDelegate handle = new SumDelegate(this.Sum);
            AsyncCallback callback = new AsyncCallback(this.OnSumCompleted);
            IAsyncResult ar = handle.BeginInvoke(1, 2, callback, null);
        }

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

        public int Sum(int x, int y)
        {
            return x + y;
        }

        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)
        {
            SumDelegate handle = new SumDelegate(this.Sum);
            AsyncCallback callback = new AsyncCallback(this.OnSumCompleted);
            IAsyncResult ar = handle.BeginInvoke(1, 2, null, null);
            int value = handle.EndInvoke(ar);
            Debug.Assert(value == 3);
        }

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

        public int Sum(int x, int y)
        {
            return x + y;
        }
當調用EndInvoke時,必須把BeginInvoke返回的IAsyncResult對象作爲參數傳遞,這樣EndInvoke纔可以通過IAsyncResult對象得知要等待哪個方法異步執行完畢。因爲在BeginInvoke返回的IAsyncResult中,屬性AsyncWaitHandle指示了用於等待異步執行完畢的一個句柄。如果你調用了很多次BeginInvoke,就會啓動很多個異步任務,每次調用返回的IAsyncResult就會對應的保存了不同的句柄。另外,這裏可以看到,EndInvoke方法的返回結果,實際上就是我們在定義SumDelegate委託時聲明的返回值類型,這個也是編譯器自動幫我們生成的。

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

因此,與之對應的,我們還有另外一個方法來等待異步執行結束,那就是我們直接訪問AsyncWaitHandle:
        private void DoMain(string cmd, string[] args)
        {
            SumDelegate handle = new SumDelegate(this.Sum);
            AsyncCallback callback = new AsyncCallback(this.OnSumCompleted);
            IAsyncResult ar = handle.BeginInvoke(1, 2, null, null);
            if(!ar.IsCompleted)
            {
                ar.AsyncWaitHandle.WaitOne();
            }
            // 異步調用已結束。
            Debug.Assert(ar.IsCompleted);
        }

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

        public int Sum(int x, int y)
        {
            return x + y;
        }

實際上,這個方式跟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函數而聲明一個新的委託了。


本篇文章來源於 精品文章收藏網-IT技術寶藏 原文鏈接:http://www.5i-net.cn/article/html/2008-04/1313p2.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章