1. 程式人生 > >C# Delegate 如何從子執行緒修改UI執行緒 主執行緒介面

C# Delegate 如何從子執行緒修改UI執行緒 主執行緒介面

一、為什麼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方法,通過一個委託把呼叫封送到控制元件所屬的執行緒上執行。

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並沒有開闢新的執行緒完成委託任務,而是讓介面控制元件的所屬執行緒完成委託任務的。看來非同步操作就是開闢新執行緒的說法不一定準確。

Test實戰:


changeBtnText c;
 public Form1()
        {
            InitializeComponent();
            GxIAPINET.IGXFactory.GetInstance().Init();
            refreshcamer_Click();
            //使用雙緩衝,讓影象顯示不閃爍
            SetStyle(
                     ControlStyles.OptimizedDoubleBuffer
                     | ControlStyles.ResizeRedraw
                     | ControlStyles.Selectable
                     | ControlStyles.AllPaintingInWmPaint
                     | ControlStyles.UserPaint
                     | ControlStyles.SupportsTransparentBackColor,
                     true);
            c = new changeBtnText(flashTextChange);
        }

 public void flashTextChange(String text) 
        {

            this.pupil_flash_btn.Text = text;
        }

//----回撥----
public void onFramecallback(object obj, IFrameData objIFrameData)
        {
            lock (obj1) {

                f1++;
                m_objCFps1.IncreaseFrameNum();
                //foreach (Point p in gbm1.pointlist) {
                //    Console.WriteLine(p.X);
                //}
                    //if (f1 % 10 == 0)
                    //{
                        //gbm1.Myfuseshow(objIFrameData);
                    double radius = 0;
                    Point p = new Point();
                    byte[] leftpupil = gbm1.getByteImg(objIFrameData, out p, out  radius, f1, calculatewhat);
                    //gbm1.Show(objIFrameData);
                   
                   // ped.PdLeftImage.Add(leftpupil);
                    ped.PdLeftPointData.Add(p);
                    ped.PdLeftRadius.Add(radius);
                    if (calculatewhat == 2)
                    {
                        int flashnumleft = ped.PdLeftRadius.Count;
                        //Console.WriteLine(flashnumleft);
                        if (flashnumleft == 1500)
                        {
                            suc_num_global_flash_num++;
                        }
                        if (suc_num_global_flash_num == 2)
                        {
                            sw.Stop();
                            Console.WriteLine("雙眼均 以完成 1500幀" + ",用時:" + sw.ElapsedMilliseconds.ToString());
                            //this.pupil_flash_btn.Text = "瞳孔對光反射關閉";
                            this.BeginInvoke(c,"哈哈哈");
                            calculatewhat = 0;
                        }
                    }

                    //if (calculatewhat == 1 || calculatewhat == 2)
                    //     this.lefteyepicturebox.Image = CreateBitmap(leftpupil, gbm1.m_nWidth, gbm1.m_nHeigh);
                    //byte[] leftpupil = gbm1.m_byMonoBuffer;
                    //Console.WriteLine("--X:"+p.X+"--Y:"+p.Y+"--R:"+radius);
                    //leftCameraImage = gbm1.SaveBmp(objIFrameData);
                    //}
                
            }
        }