非UI線程更新UI界面的各種方法小結


我們知道只有UI線程才能更新UI界面,其他線程訪問UI控件被認為是非法的。但是我們在進行異步操作時,經常需要將異步執行的進度報告給用戶,讓用戶知道任務的進度,不至於讓用戶誤認為程序“死掉了”,特別是對於Winform,WPF等客戶端程序尤為重要。

  那么我們要探討的就是如何讓非UI的任務線程更新UI界面。下面對已知的幾種實現方式做個總結。隨着.Net版本的不斷升級,實現方式還可能會增加。

1)使用Control.Invoke或Control.BeginInvoke。

.Net1.1時允許非UI的線程訪問UI控件,.Net2.0開始不允許了。所以程序員首先要檢測Control的InvokeRequired屬性,如果為true,就說明是非UI線程訪問了這個控件,於是就需要調用這兩個方法之一,將操作UI的函數封裝到UI線程上去執行。其中Invoke是阻塞的,BeginInvoke是異步的。

        private delegate void ProgressChangedHander(int percentage);
        private void UpdateUI(int percentage)
        {
            if (this.progressBar1.InvokeRequired)
            {
                //非UI線程,再次封送該方法到UI線程
                this.progressBar1.BeginInvoke(new ProgressChangedHander(UpdateUI), new object[] { percentage });
            }
            else
            {
                //UI線程,進度更新
                this.progressBar1.Value = percentage;
            }        
        }

2)利用同步上下文調度器

.Net4.0增加了一個線程操作的類Task。Task的Start方法或ContinueWith方法中可以指定一個任務調度器TaskScheduler,如果這個任務調度器是同步上下文調度器,那么在Task的方法中就可以訪問UI控件。要得到一個同步上下文調度器,需要通過TaskScheduler的靜態方法FromCurrentSynchronizationContext。

            //得到一個同步上下文調度器
            TaskScheduler syncSch = TaskScheduler.FromCurrentSynchronizationContext();

            Task<int> t = new Task<int>(() => Sum(100));

            //在Task的ContinueWith方法中,指定這個同步上下文調度器,我們更新了form的Text屬性
            //去掉這個syncSch,你就會發現要出異常
            t.ContinueWith(task => Text = task.Result.ToString(), syncSch);
            t.Start();

PS: 其實TaskScheduler內部是使用SynchronizationContext實現的。

3)利用同步上下文SynchronizationContext

這個類很重要,利用這個類可以大大簡化我們的異步更新UI界面的代碼。避免了和線程間的無盡糾纏。利用SynchronizationContext的Current可以得到當前線程的同步上下文。注意,如果你在非UI線程上調用,會得到null。所以我們需要在UI線程上首先得到它的一個引用。然后在任務線程里就可以用這個引用變量。利用它的Send或Post方法將我們的更新UI的函數封送到UI線程上執行。對於WinForm程序來說Current返回的是WindowsFormsSynchronizationContext,它是SynchronizationContext的一個子類。Send或Post方法內部其實還是使用的Control.Invoke或Control.BeginInvoke來實現的。看一下它的Send方法:

public override void Send(SendOrPostCallback d, object state)
{
    Thread destinationThread = this.DestinationThread;
    if (destinationThread == null || !destinationThread.IsAlive) throw new InvalidAsynchronousStateException(SR.GetString("ThreadNoLongerValid"));
    //這里就是用的control的invoke方法
    if (this.controlToSendTo != null) this.controlToSendTo.Invoke(d, new object[] { state });
}

注意:Send方法是阻塞的,Post方法是異步的。

喜歡刨根問底的,比如我,又在想,Control的Invoke是如何實現線程間的封送的呢?我們來略微調查一下。

public object Invoke(Delegate method, params object[] args)
{
    using (new MultithreadSafeCallScope())
    {
        return this.FindMarshalingControl().MarshaledInvoke(this, method, args, true);
    }
}

Invokie里調用了MarshaledInvoke方法。一看Marshal就知道有封送的意思。為了不偏離主題,對MarshaledInvoke這個方法的代碼保留主要的部分,有個印象就行,大家不用太較真,畢竟是Mircrosoft內部的代碼,沒太多的閑工夫來研究。

private object MarshaledInvoke(Control caller, Delegate method, object[] args, bool synchronous)
{
    int num;
    //
    bool flag = false;
   //判斷是不是UI線程調用的Invoke 
if (SafeNativeMethods.GetWindowThreadProcessId(new HandleRef(this, this.Handle), out num) == SafeNativeMethods.GetCurrentThreadId() && synchronous) flag = true;
    ExecutionContext executionContext = null;
    //如果不是,獲得UI線程的執行上下文
    if (!flag) executionContext = ExecutionContext.Capture();
    //利用這個UI線程的上下文,構造一個線程調用方法入口
    ThreadMethodEntry entry = new ThreadMethodEntry(caller, this, 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
        UnsafeNativeMethods.PostMessage(new HandleRef(this, this.Handle), threadCallbackMessage, IntPtr.Zero, IntPtr.Zero);//異步:發送消息,UI得到消息就會調用
    if (!synchronous) return entry;
    if (!entry.IsCompleted) this.WaitForWaitHandle(entry.AsyncWaitHandle);
    if (entry.exception != null) throw entry.exception;
    return entry.retVal;
}

上面的方法的內部實現較為復雜,勉強注釋了幾個地方,一家之言,不可全信。大意可能大家都明白了,對於BeginInvoke異步調用,它用了消息泵,UI線程可以提取到這個消息,並執行相應的函數。而對於同步的Invoke忍不住又查了點:

ExecutionContext.Run(tme.executionContext, invokeMarshaledCallbackHelperDelegate, tme);

這里的tme就是ThreadMethodEntry,說明ExecutionContext的靜態方法Run是不是實現了線程的切換呢?不再繼續調查了,我們只用記住,Control的Invoke和BeginInvoke可以實現到UI線程的切換就行了。

說着說着就遠離主題了,下面來看看SynchronizationContext的用法:

       private void SyncContextTest()
        {
            //UI線程的ISynchronizationContext取得
            SynchronizationContext syncContext = SynchronizationContext.Current;

            //新建一個模擬操作i
            ThreadPool.QueueUserWorkItem((o) =>
                {
                    for (int i = 0; i < 100; i++)
                    {
                        //模擬耗時
                        Thread.Sleep(100);
                        //通知用戶
                        syncContext.Post(new SendOrPostCallback(ProgressCallBack), i);
                    }
                }
              );               
        }
        private void ProgressCallBack(object percent)
        {
            //不再判定是不是UI線程
            this.progressBar1.Value = (int)percent;
        }

  但是上面的代碼還是有點缺陷,就是Post的回調函數參數只能是object的,要強行轉換成int。但我們可以像下面這樣修改,為用戶提供一個int型的接口。

        delegate void UserNotifyProcess(int percent);
        private void SyncContextTest()
        {
            // UI線程的ISynchronizationContext取得
            SynchronizationContext syncContext = SynchronizationContext.Current;

            UserNotifyProcess userNotify = null;
            userNotify += new UserNotifyProcess(ProgressCallBack);

            //新建一個模擬操作i
            ThreadPool.QueueUserWorkItem((o) =>
                {
                    for (int i = 0; i < 100; i++)
                    {
                        //模擬耗時
                        Thread.Sleep(100);
                        //通知用戶
                        syncContext.Post((param) =>
                        {
                            //這里是關鍵了,只要到這里就說明是UI線程了
                            if (userNotify != null)
                            {
                                userNotify((int)param);
                            }
                        },
                        i);
                    }
                }
              );
        }

上面的代碼只是一個測試代碼,具體應該封裝到一個類中,以提供事件的方式公開這個接口。

關於SynchronizationContext的詳細闡述,可以看看這篇很有價值的文章:

http://www.codeproject.com/Articles/31971/Understanding-SynchronizationContext-Part-I

http://www.codeproject.com/Articles/32113/Understanding-SynchronizationContext-Part-II

http://www.codeproject.com/Articles/32119/Understanding-SynchronizationContext-Part-III

4)使用Control.CheckForIllegalCrossThreadCalls

Control類的靜態成員CheckForIllegalCrossThreadCalls設置為false,UI線程創建的控件,允許被非UI線程訪問,但是這種做法極度不推薦,這樣會導致線程不安全。隨着.Net版本的升級,這個屬性很可能被禁用。不過暫時還可以使用。看代碼:

        public Form1()
        {
            CheckForIllegalCrossThreadCalls = false;//直接在構造函數加上這句就行了。
            InitializeComponent();
        }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM