TIPS-About UI Thread Limitation

這是一個程式"中鳥"開發Windows Form要面對的問題...

Windows Form裡的Threading有些討厭的限制。菜鳥還處於天真無邪的Single Thread打天下階段,渾然不知它的險惡;而老鳥早就吃過苦頭,熟知要如何對付它,所以也不畏懼它的刁難。而剛開始學會在Windows Form中展現Multi-threading威力的中鳥,一起步多半要先闖過這一關。

用一個最簡單的例子來說明好: 我寫了一個Windows Form,放了一個label1一個button1, 在button1_Click事件另外建立一個Thread呼叫chgLabel(text)修改label1的Text屬性:

        private void button1_Click(object sender, EventArgs e)
        {
            Thread trd = new Thread(new ThreadStart(jobBad));
            trd.Start();
        }
 
        private void jobBad() 
        {
            chgLabel("Wrong way.");
        }
 
        private void chgLabel(string text)
        {
            label1.Text = text;
        }

程式碼很簡單,看起來應該沒什麼問題吧?

如果你這麼想,表示你在Windows Form的世界還涉世未深...

這段程式觸犯了Windows Form裡的一條鐵律: 在非UI Thread裡更動了UI的內容!

從沒用C++, Windows SDK角度探索Windows Application Model的我,並不知道它的細節原由,但歷經多次慘痛的經驗教訓,我學到一點: 如果你另外開了一條Thread,程式碼如會變更到Windows Form上的Control內容,請記得繞道而行。在上述的例子中,我們很明確地修改了Label的內容,但有時程式碼會在你不知情的情況下更動了UI元素。最典型的例子是DataGrid Bind到一個DataTable,而我們在另一條Thread中表面上只更改了DataTable的內容,並沒有修改UI上的Control;但由於Data Binding的關係,一更動DataTable就會間接觸發了修改DataGrid顯示的事件,進而違反了在非UI Thread修改UI內容的規則。

記得在.NET 1.1時代,違反這條規則並非絕對會出錯,而是偶爾冒出Null Reference Exception讓你丈二金剛摸不著腦袋,完全不知自己犯了什麼天條遭此報應。.NET 2.0比較仁道一點,會很明確地彈出以下的訊息:
Invalid Operation Exception:
Cross-thread operation not valid: Control 'label1' accessed from a thread other than the thread it was created on.

這張罰單把違規事由寫得清清楚楚,想當初由Null Reference去追出問題出在UI Thread限制就讓我吃盡苦頭,由.NET 2.0入門的朋友算是幸福多了。

Good! 知道問題後,要怎麼修改? 官方的標準答案是宣告一個deleate, 再用InvokeBeginInvoke來觸發它,這樣可以強迫Windows切回UI Thread來執行程式

        private void button1_Click(object sender, EventArgs e)
        {
            Thread trd = new Thread(new ThreadStart(jobGood));
            trd.Start();
        }
 
        private void chgLabel(string text)
        {
            label1.Text = text;
        }
 
        delegate void ChgLabelHandler(string text);
        private void jobGood()
        {
            this.Invoke(new ChgLabelHandler(chgLabel), "Correct way.");
        }

還有一些進階的課題,例如: 某些Code可能被UI Thread呼叫,也可能被非UI Thread執行,所以可以用InvokeRequired來偵測,決定要直接變更UI內容或是使用Invoke,這裡就先不要搞得太複雜,以免嚇到大家。先記住一點,當你遇到"Cross-thread operation not valid",要知道發生了什麼事以及該怎麼辦,就夠了。

歡迎推文分享:
Published 30 September 2007 10:33 AM 由 Jeffrey
Filed under: ,
Views: 45,718



意見

# kennyshu said on 22 October, 2007 01:46 PM

我在菜鳥的時候就遇到了這個問題(現在還是菜) >"<

之前用.Net 1.1的時候(也就是VS2003)直接改UI上的control都沒有問題,升級到2.0後就出現一堆問題了,當時真的覺得2.0比1.1還不好用…

研究了好久並且請教一些先進之後,目前我是用BeginInvoke + MethodInvoker來解決這個問題。

# Robert said on 24 October, 2007 04:32 AM

除了比較複雜的狀況, 我都用BackgroupWorker這個Component. 感覺上不用傷腦筋, Code也比較好看...ㄟ...不過可能也是我比較懶惰吧...哈

# Jeffrey said on 24 October, 2007 04:55 AM

To Robert, 沒錯,.NET 2.0的BackgroundWorker又是一個"傑克,這真的是太神奇了"級的新發明,事實上我也正在寫一篇相關的文章。

現在的程式開發人員需要懂的東西愈來愈少,卻可以做出同樣複雜的東西,真是幸福,但相對也少了深入了解背後原理的機會,某種角度來說也是種不幸吧?? (謎之聲: 只有你這種愛鑽牛角尖的賤骨頭才會這樣想吧? 哈!)

# Ken Wu said on 20 November, 2007 05:06 PM

錯別字deleate -> delegate

推薦幾篇2002~2004年相關的官方文章:

msdn2.microsoft.com/.../ms998490.aspx

msdn2.microsoft.com/.../ms996402.aspx

msdn2.microsoft.com/.../ms996483.aspx

# SGY said on 10 April, 2008 09:10 AM

Test .

Runtime 可執行Change UI !! (真的改變了)

Debug Mode: 會Error!!

# SGY said on 11 April, 2008 09:31 AM

When App 在 TaskTray  

  this.Visual =false

When App 不在 TaskTray  

  this.Visual =false

Use thread Update UI時要小心

       private void button1_Click(object sender, EventArgs e)        

{            

Thread trd = new Thread(new ThreadStart(jobGood));            

trd.Start();        

}        

private void chgLabel(string text)        

{            

label1.Text = text;        

}        

delegate void ChgLabelHandler(string text);        

private void jobGood()        

{            

this.Invoke(new ChgLabelHandler(chgLabel), "Correct way.");        

}

改良一下

private void jobGood()        

{

           if (this.Visible)

           {

              this.Invoke(new ChgLabelHandler(chgLabel), "Correct way.");  

           }

           else

           {

               chgLabel("Correct way.");

           }          

}

這樣就完美了 ^^

By:SGY

# Jeffrey said on 11 April, 2008 11:06 AM

to SGY, 謝謝你的補充,我有另一篇較完整的探討文章,也歡迎你參考指教。

blog.darkthread.net/.../better-winform-ui.aspx

# Samuel said on 13 January, 2009 02:14 AM

請問在 .net C++ 要如何做出此功能

# Pao said on 04 March, 2009 06:30 AM

謝謝!

寫的簡潔有利

對我這菜鳥幫助頗大

感恩

# vix said on 07 January, 2011 01:09 AM

請問前輩

我最近就遇到這個問題

我在另一個thread裡調用一個函式

裡面用了 AnimateWindow(this.Handle, 1000, AW_SLIDE + AW_VER_NEGATIVE);

錯誤就出現在這

當我在建構式加上Form.CheckForIllegalCrossThreadCalls = false;

沒出現問題

但是視窗在下降時

AnimateWindow(this.Handle,1000,AW_SLIDE + AW_VER_POSITIVE + AW_HIDE);

rect會畫錯而產生畫面好像抖動感

不知前輩能給些意見嗎?

感激不盡

# vix said on 07 January, 2011 03:00 AM

忘了說小的用的是System.timers

謝謝

# Jeffrey said on 08 January, 2011 10:40 AM

to vix, 不太確定你AnimateWindow的寫法,但依我的直覺,應該是AnimateWindow的執行效率還不夠好,所以重新產生畫面時產生抖動感,不過還能有沒有改良空間就很難說了。

# vix said on 11 January, 2011 09:44 PM

感謝前輩指點,問題已經解決

但是我目前遇到另一個問題

因為看了一些網路上的文提到

使用Form.CheckForIllegalCrossThreadCalls = false;

是不安全的(其實我不清楚不安全的地方是什麼)

所以改以另一個thread去執行delegate的方式

delegate裡跑的是使用invoke()去跑包含了animatewindow的函式

但是這樣當執行到animatewindow函式時

一樣佔用了UI Thread

所有form的操作還是要等到animatewindow執行完才能再操作

不知道是我觀念錯誤寫錯了

還是invoke把animatewindow拉回UI Thread執行不可避免的會佔掉

還是有什麼辨法能讓animatewindow在另一個thread執行而沒有警告發生呢?

謝謝前輩

# Jeffrey said on 12 January, 2011 12:29 AM

to vix, 依我淺薄的WinForm開發經驗(我WebForm玩的比較多),跨Thread去動UI的東西會產生"在某些你不知道的特定條件下不定期出錯"的問題,這種Bug查起來叫人很想死。所以,其實我很贊成設定CheckForIllegalCrossThreadCalls=true,可以減少誤入險境的機會。

Invoke時,就是將工作交給UI Thread去處理,UI Thread只有一條,所以排隊執行是免不了的,依你說的情況,我想得到的策略是將耗時的運算交給其他Thread去處理,只留最後真正要更動UI元素的一小部分再用Invoke,儘可能減少佔用UI Thread的時間。

你的看法呢?

(必要的) 
(必要的) 
(選擇性的)
(必要的) 
(提醒: 因快取機制,您的留言幾分鐘後才會顯示在網站,請耐心稍候)

5 + 3 =

搜尋

Go

<September 2007>
SunMonTueWedThuFriSat
2627282930311
2345678
9101112131415
16171819202122
23242526272829
30123456
 
RSS
創用 CC 授權條款
【廣告】
twMVC

Tags 分類檢視
關於作者

一個醉心技術又酷愛分享的Coding魔人,十年的IT職場生涯,寫過系統、管過專案, 也帶過團隊,最後還是無怨無悔地選擇了技術鑽研這條路,近年來則以做一個"有為的中年人"自許。

文章典藏
其他功能

這個部落格


Syndication