C#的Invoke和BeginInvoke

來源:互聯網
上載者:User

標籤:blog   http   使用   os   strong   資料   io   for   

在Invoke或者BeginInvoke的使用中無一例外地使用了委託Delegate,至於委託的本質請參考我的另一隨筆:對.net事件的看法。
  

一、為什麼Control類提供了Invoke和BeginInvoke機制?

關於這個問題的最主要的原因已經是dotnet程式員眾所周知的,我在此費點筆墨再次記錄到自己的日誌,以便日後提醒一下自己。

1、windows程式訊息機制

Windows GUI程式是基於訊息機制的,有個主線程維護著一個訊息泵。這個訊息泵讓windows程式生生不息。

 

                                                  Windows GUI程式的訊息迴圈

 

 

 

Windows程式有個訊息佇列,表單上的所有訊息是這個隊列裡面訊息的最主要來源。這裡的while迴圈使用了GetMessage()這個方法,這是個阻塞方法,也就是隊列為空白時方法就會被阻塞,從而這個while迴圈停止運動,這避免了一個程式把cpu無緣無故地耗盡,讓其它程式難以得到響應。當然在某些需要cpu最大限度運動的程式裡面就可以使用另外的方法,例如某些3d遊戲或者及時戰略遊戲中,一般會使用PeekMessage()這個方法,它不會被windows阻塞,從而保證整個遊戲的流暢和比較高的幀速。

這個主線程維護著整個表單以及上面的子控制項。當它得到一個訊息,就會調用DispatchMessage方法派遣訊息,這會引起對表單上的視窗過程的調用。視窗過程裡面當然是程式員提供的表單資料更新代碼和其它代碼。

2、dotnet裡面的訊息迴圈

public static void Main(string[] args)

{

   Form f = new Form();

   Application.Run(f);

}

Dotnet表單程式封裝了上述的while迴圈,這個迴圈就是通過Application.Run方法啟動的。

3、線程外操作GUI控制項的問題

如果從另外一個線程操作windows表單上的控制項,就會和主線程產生競爭,造成不可預料的結果,甚至死結。因此windows GUI編程有一個規則,就是只能通過建立控制項的線程來操作控制項的資料,否則就可能產生不可預料的結果。

因此,dotnet裡面,為了方便地解決這些問題,Control類實現了ISynchronizeInvoke介面,提供了Invoke和BeginInvoke方法來提供讓其它線程更新GUI介面控制項的機制。

public interface ISynchronizeInvoke

{

        [HostProtection(SecurityAction.LinkDemand, Synchronization=true, ExternalThreading=true)]

        IAsyncResult BeginInvoke(Delegate method, object[] args);

        object EndInvoke(IAsyncResult result);

        object Invoke(Delegate method, object[] args);

        bool InvokeRequired { get; }

}

}

如果從線程外操作windows表單控制項,那麼就需要使用Invoke或者BeginInvoke方法,通過一個委託把調用封送到控制項所屬的線程上執行。

二、訊息機制---線程間和處理序間通訊機制

1、window訊息發送

Windows訊息機制是windows平台上的線程或者處理序間通訊機制之一。Windows訊息值其實就是定義的一個資料結構,最重要的是訊息的類型,它就是一個整數;然後就是訊息的參數。訊息的參數可以表示很多東西。

Windows提供了一些api用來向一個線程的訊息佇列發送訊息。因此,一個線程可以向另一個線程的訊息佇列發送訊息從而告訴對方做什麼,這樣就完成了線程間的通訊。有些api發送訊息需要一個視窗控制代碼,這種函數可以把訊息發送到指定視窗的主線程訊息佇列;而有些則可以直接通過線程控制代碼,把訊息發送到該線程訊息佇列中。

                                            

 

用訊息機制通訊

  

SendMessage是windows api,用來把一個訊息發送到一個視窗的訊息佇列。這個方法是個阻塞方法,也就是作業系統會確保訊息的確發送到目的訊息佇列,並且該訊息被處理完畢以後,該函數才返回。返回之前,調用者將會被暫時阻塞。

PostMessage也是一個用來發送訊息到視窗訊息佇列的api函數,但這個方法是非阻塞的。也就是它會馬上返回,而不管訊息是否真的發送到目的地,也就是調用者不會被阻塞。

2、Invoke and BeginInvoke

 

 

                                                        Invoke or BeginInvoke

  

Invoke或者BeginInvoke方法都需要一個委派物件作為參數。委託類似於回呼函數的地址,因此調用者通過這兩個方法就可以把需要調用的函數地址封送給介面線程。這些方法裡面如果包含了更改控制項狀態的代碼,那麼由於最終執行這個方法的是介面線程,從而避免了競爭條件,避免了不可預料的問題。如果其它線程直接操作介面線程所屬的控制項,那麼將會產生競爭條件,造成不可預料的結果。

使用Invoke完成一個委託方法的封送,就類似於使用SendMessage方法來給介面線程發送訊息,是一個同步方法。也就是說在Invoke封送的方法被執行完畢前,Invoke方法不會返回,從而調用者線程將被阻塞。

使用BeginInvoke方法封送一個委託方法,類似於使用PostMessage進行通訊,這是一個非同步方法呼叫。也就是該方法封送完畢後馬上返回,不會等待委託方法的執行結束,調用者線程將不會被阻塞。但是調用者也可以使用EndInvoke方法或者其它類似WaitHandle機制等待非同步作業的完成。

但是在內部實現上,Invoke和BeginInvoke都是用了PostMessage方法,從而避免了SendMessage帶來的問題。而Invoke方法的同步阻塞是靠WaitHandle機制來完成的。

3、使用場合問題

如果你的後台線程在更新一個UI控制項的狀態後不需要等待,而是要繼續往下處理,那麼你就應該使用BeginInvoke來進行非同步處理。

如果你的後台線程需要操作UI控制項,並且需要等到該操作執行完畢才能繼續執行,那麼你就應該使用Invoke。否則,在後台線程和主截麵線程共用某些狀態資料的情況下,如果不同步調用,而是各自繼續執行的話,可能會造成執行序列上的問題,雖然不發生死結,但是會出現不可預料的顯示結果或者資料處理錯誤。

可以看到ISynchronizeInvoke有一個屬性,InvokeRequired。這個屬性就是用來在編程的時候確定,一個對象訪問UI控制項的時候是否需要使用Invoke或者BeginInvoke來進行封送。如果不需要那麼就可以直接更新。在調用者對象和UI對象同屬一個線程的時候這個屬性返回false。在後面的程式碼分析中我們可以看到,Control類對這一屬性的實現就是在判斷調用者和控制項是否屬於同一個線程的。

三、Delegate.BeginInvoke

通過一個委託來進行同步方法的非同步呼叫,也是.net提供的非同步呼叫機制之一。但是Delegate.BeginInvoke方法是從ThreadPool取出一個線程來執行這個方法,以獲得非同步執行效果的。也就是說,如果採用這種方式提交多個非同步委託,那麼這些調用的順序無法得到保證。而且由於是使用線程池裡面的線程來完成任務,使用頻繁,會對系統的效能造成影響。

Delegate.BeginInvoke也是講一個委託方法封送到其它線程,從而通過非同步機制執行一個方法。調用者線程則可以在完成封送以後去繼續它的工作。但是這個方法封送到的最終執行線程是運行庫從ThreadPool裡面選取的一個線程。

這裡需要糾正一個誤區,那就是Control類上的非同步呼叫BeginInvoke並沒有開闢新的線程完成委託任務,而是讓介面控制項的所屬線程完成委託任務的。看來非同步作業就是開闢新線程的說法不一定準確。  

四、用Reflector察看一些相關代碼

1、Control.BeginInvoke and Control.Invoke

public IAsyncResult BeginInvoke(Delegate method, params object[] args)

{

    using (new MultithreadSafeCallScope())

    {

        return (IAsyncResult) this.FindMarshalingControl().MarshaledInvoke(this, method, args, false);

    }

}

public object Invoke(Delegate method, params object[] args)

{

    using (new MultithreadSafeCallScope())

    {

        return this.FindMarshalingControl().MarshaledInvoke(this, method, args, true);

    }

}

這裡的FindMarshalingControl方法通過一個迴圈向上回溯,從當前控制項開始回溯父控制項,直到找到最頂級的父控制項,用它作為封送對象。例如,我們調用表單上一個進度條的Invoke方法封送委託,但是實際上會回溯到主表單,通過這個控制項對象來封送委託。因為主表單是主線程訊息佇列相關的,發送給主表單的訊息才能發送到介面主線程訊息佇列。

我們可以看到Invoke和BeginInvoke方法使用了同樣的實現,只是MarshaledInvoke方法的最後一個參數值不一樣。

2、MarshaledInvoke

private object MarshaledInvoke(Control caller, Delegate method, object[] args, bool synchronous)

{

    int num;

    if (!this.IsHandleCreated)

    {

        throw new InvalidOperationException(SR.GetString("ErrorNoMarshalingThread"));

    }

    if (((ActiveXImpl) this.Properties.GetObject(PropActiveXImpl)) != null)

    {

        IntSecurity.UnmanagedCode.Demand();

    }

    bool flag = false;

    if ((SafeNativeMethods.GetWindowThreadProcessId(new HandleRef(this, this.Handle), out num) == SafeNativeMethods.GetCurrentThreadId()) && synchronous)

    {

        flag = true;

    }

    ExecutionContext executionContext = null;

    if (!flag)

    {

        executionContext = ExecutionContext.Capture();

    }

    ThreadMethodEntry entry = new ThreadMethodEntry(caller, method, args, synchronous, executionContext);

    lock (this)

    {

        if (this.threadCallbackList == null)

        {

            this.threadCallbackList = new Queue();

        }

    }

    lock (this.threadCallbackList)

    {

        if (threadCallbackMessage == 0)

        {

            threadCallbackMessage = SafeNativeMethods.RegisterWindowMessage(Application.WindowMessagesVersion + "_ThreadCallbackMessage");

        }

        this.threadCallbackList.Enqueue(entry);

    }

    if (flag)

    {

        this.InvokeMarshaledCallbacks();

    }

    else

    {            //終於找到你了,PostMessage

        UnsafeNativeMethods.PostMessage(new HandleRef(this, this.Handle), threadCallbackMessage, IntPtr.Zero, IntPtr.Zero);

    }

    if (!synchronous) //如果是非同步,那麼馬上返回吧

    {

        return entry;

    }

    if (!entry.IsCompleted) //同步調用沒結束,阻塞起來等待吧

    {

        this.WaitForWaitHandle(entry.AsyncWaitHandle);

    }

    if (entry.exception != null)

    {

        throw entry.exception;

    }

    return entry.retVal;

}

怎麼樣,我們終於看到PostMessage了吧?通過windows訊息機制實現了封送。而需要封送的委託方法作為訊息的參數進行了傳遞。關於其它的代碼這裡不作進一步解釋。

3、InvokeRequired

public bool InvokeRequired

{

    get

    {

        using (new MultithreadSafeCallScope())

        {

            HandleRef ref2;

            int num;

            if (this.IsHandleCreated)

            {

                ref2 = new HandleRef(this, this.Handle);

            }

            else

            {

                Control wrapper = this.FindMarshalingControl();

                if (!wrapper.IsHandleCreated)

                {

                    return false;

                }

                ref2 = new HandleRef(wrapper, wrapper.Handle);

            }

            int windowThreadProcessId = SafeNativeMethods.GetWindowThreadProcessId(ref2, out num);

            int currentThreadId = SafeNativeMethods.GetCurrentThreadId();

            return (windowThreadProcessId != currentThreadId);

        }

    }

}

終於看到了,這是在判斷windows表單線程和當前的調用者線程是否是同一個,如果是同一個就沒有必要封送了,直接存取這個GUI控制項吧。否則,就不要那麼直接表白了,就需要Invoke或者BeginInvoke做媒了

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.