詳解Winform多線(xiàn)程編程基本原理
本文在這里將從.NET并行計(jì)算講起,主要環(huán)境為Winform多線(xiàn)程編程。希望通過(guò)本文能對(duì)大家了解Winform多線(xiàn)程編程有所幫助,用好.NET并行計(jì)算。51CTO向您推薦《WinForm應(yīng)用與開(kāi)發(fā)視頻教程-WinForm教程》
首先我們創(chuàng)建一個(gè)Winform的應(yīng)用程序,在上面添加一個(gè)多行文本框和一個(gè)按鈕控件,按鈕的事件如下:
- Thread.Sleep(1000);
- StringBuilder sb = new StringBuilder();
- for (int i = 0; i < 10000; i++)
- sb.Append("test");
- string s = sb.ToString();
- textBox1.Text = s;
首先我們可以把這個(gè)操作理解為一個(gè)非常耗時(shí)的操作,它至少占用1秒的時(shí)間。在1秒后,我們整了一個(gè)大字符串作為文本框的值,然后在標(biāo)簽上顯示給文本框賦值這個(gè)UI渲染行為需要的時(shí)間,程序執(zhí)行結(jié)果如下:

我們可以感受到,在點(diǎn)擊了按鈕之后整個(gè)程序的UI就卡住了,沒(méi)有辦法拖動(dòng)沒(méi)有辦法改變大小,用于體驗(yàn)非常差。一般能想到會(huì)新建一個(gè)線(xiàn)程來(lái)包裝這個(gè)方法,使得UI線(xiàn)程不被卡住:
- new Thread(() =>
- {
- Thread.Sleep(1000);
- StringBuilder sb = new StringBuilder();
- for (int i = 0; i < 10000; i++)
- sb.Append("test");
- string s = sb.ToString();
- textBox1.Text = s;
- }).Start();
使用調(diào)試方式運(yùn)行程序的話(huà)會(huì)得到如下的異常(非調(diào)試方式不會(huì)):

雖然我們知道這樣設(shè)置:
- Control.CheckForIllegalCrossThreadCalls = false;
可以屏蔽這個(gè)錯(cuò)誤,但是在非創(chuàng)建控件的線(xiàn)程去更新控件的狀態(tài)的做法會(huì)導(dǎo)致很多問(wèn)題,比如死鎖和控件部分被更新等。微軟推薦我們使用Control的Invoke或BeginInvoke方法來(lái)把涉及到控件狀態(tài)更新的操作讓UI線(xiàn)程去做:
- new Thread(() =>
- {
- Invoke(new Action(() =>
- {
- Thread.Sleep(1000);
- StringBuilder sb = new StringBuilder();
- for (int i = 0; i < 10000; i++)
- sb.Append("test");
- string s = sb.ToString();
- textBox1.Text = s;
- }));
- }).Start();
你可能會(huì)想到這么寫(xiě),但是運(yùn)行程序后可以發(fā)現(xiàn)界面依然是卡死。想一下,雖然我們新開(kāi)了一個(gè)線(xiàn)程,但是馬上又把整個(gè)代碼段交給UI線(xiàn)程去做了,當(dāng)然起不到效果。其實(shí)這個(gè)方法的工作可以分為兩部分,一部分是我們數(shù)據(jù)的計(jì)算,一部分是把計(jì)算好的數(shù)據(jù)顯示在界面上,我們只應(yīng)該把真正和UI相關(guān)的操作放到Invoke中讓UI線(xiàn)程去做:
- new Thread(() =>
- {
- Thread.Sleep(1000);
- StringBuilder sb = new StringBuilder();
- for (int i = 0; i < 10000; i++)
- sb.Append("test");
- string s = sb.ToString();
- Invoke(new Action(() =>
- {
- textBox1.Text = s;
- }));
- }).Start();
再測(cè)試一次可以發(fā)現(xiàn),UI在前1秒多的時(shí)間沒(méi)有卡死,在最后的一點(diǎn)時(shí)間還是卡死了。在繼續(xù)研究卡死問(wèn)題之前我們來(lái)看一下,Control提供了InvokeRequired屬性來(lái)讓我們判斷當(dāng)前線(xiàn)程是不是UI線(xiàn)程,或者說(shuō)當(dāng)前的操作是否需要進(jìn)行Invoke:
- textBox1.Text = this.InvokeRequired.ToString();
- new Thread(() =>
- {
- textBox1.Text += Environment.NewLine + this.InvokeRequired.ToString();
- Invoke(new Action(() =>
- {
- textBox1.Text += Environment.NewLine + this.InvokeRequired.ToString();
- }));
- }).Start();
通過(guò)非調(diào)試方式啟動(dòng)程序可以得到如下結(jié)果:

很明顯:
1) 在線(xiàn)程外的賦值不需要Invoke(在UI線(xiàn)程)
2) 在線(xiàn)程內(nèi)的賦值需要Invoke(不在UI線(xiàn)程)
3) 在Invoke中的賦值已經(jīng)封送給UI線(xiàn)程,所以不需要Invoke
繼續(xù)研究卡死問(wèn)題,您可能會(huì)想到,Control還提供了一個(gè)BeginInvoke方法,我們來(lái)試試看:
- new Thread(() =>
- {
- Thread.Sleep(1000);
- StringBuilder sb = new StringBuilder();
- for (int i = 0; i < 10000; i++)
- sb.Append("test");
- string s = sb.ToString();
- BeginInvoke(new Action(() =>
- {
- textBox1.Text = s;
- }));
- }).Start();
好像效果上還是沒(méi)什么區(qū)別,那么Invoke和BeginInvoke的區(qū)別在哪里呢?
我們知道Windows應(yīng)用程序基于消息,Windows API提供了SendMessage和PostMessage兩個(gè)API,前者執(zhí)行消息后返回(不經(jīng)過(guò)消息管道,先于PostMessage執(zhí)行),后者把消息發(fā)送到管道異步執(zhí)行。Invoke和BeginInvoke從行為上來(lái)說(shuō)類(lèi)似這兩個(gè)API,但是實(shí)際上兩者都使用了PostMessage,前者使用信號(hào)量在消息執(zhí)行前阻塞,達(dá)到同步的效果。我們來(lái)做一個(gè)實(shí)驗(yàn):
- new Thread(() =>
- {
- Thread.Sleep(1000);
- StringBuilder sb = new StringBuilder();
- for (int i = 0; i < 10000; i++)
- sb.Append("test");
- string s = sb.ToString();
- Stopwatch sw = Stopwatch.StartNew();
- Invoke(new Action(() =>
- {
- textBox1.Text = s;
- }));
- MessageBox.Show(sw.ElapsedMilliseconds.ToString());
- }).Start();
運(yùn)行程序:

可以體會(huì)到,在文本框的值出現(xiàn)之后才出現(xiàn)彈出框,文本框賦值這個(gè)消息的執(zhí)行過(guò)程耗時(shí)2秒。把Invoke改為BeginInvoke其它不動(dòng)再執(zhí)行程序:

明顯感到彈出框先顯示2秒后文本框的值出現(xiàn)。BeginInvoke沒(méi)有阻塞后續(xù)語(yǔ)句的執(zhí)行。因此,需要注意,如果我們?cè)诜椒ㄖ惺褂玫淖兞吭贐eginInvoke之后有修改,極有可能發(fā)生混亂。如果您使用過(guò)委托的BeginInvoke應(yīng)該會(huì)知道,通常建議總是調(diào)用EndInvoke來(lái)回收資源,對(duì)于Control的EndInvoke來(lái)說(shuō),如果您不需要獲取返回值的話(huà),那么它不是必須的(來(lái)自msdn)。
#T#
現(xiàn)在您可能還有疑問(wèn)為什么使用了BeginInvoke,UI還是卡了大概2秒,可以這么理解,我們把這么多的文字賦值到文本框中,這個(gè)UI行為是非常耗時(shí)的,不管是Invoke還是BeginInvoke最終是發(fā)送消息給UI線(xiàn)程處理(兩者都沒(méi)有使用線(xiàn)程池),它就是需要這么多時(shí)間,在一般情況下我們不會(huì)在UI上呈現(xiàn)這么多數(shù)據(jù)。
一般來(lái)說(shuō)我們能做的優(yōu)化是:
1) 盡量把非UI的操作使用新的線(xiàn)程去異步計(jì)算,不阻塞UI線(xiàn)程,真正需要操作UI的時(shí)候才去提交給UI線(xiàn)程
2) 盡量減少UI的操作復(fù)雜度,比如如果需要在UI上繪制一個(gè)復(fù)雜圖形可以在內(nèi)存中先創(chuàng)建一個(gè)位圖,繪制好之后把整個(gè)位圖在UI上繪制,而不是直接在UI上繪制這個(gè)圖形
舉個(gè)例子,UI就好象一塊畫(huà)布,我們要在上面畫(huà)一個(gè)巨作怎么才能不過(guò)多占用這塊布的時(shí)間,讓大家都能用上呢?一個(gè)方法就是我們?cè)跍?zhǔn)備顏色和畫(huà)筆的時(shí)候不占著這個(gè)布,真正要去畫(huà)的時(shí)候才去用,另外一個(gè)方法就是在另一塊畫(huà)布上先畫(huà),然后把圖案采用復(fù)印的方式印到我們的主畫(huà)布上。
對(duì)于大量數(shù)據(jù)的呈現(xiàn),我們還可以:
1) 采用分頁(yè),只顯示一部分?jǐn)?shù)據(jù),對(duì)于Windows程序的分頁(yè)可能就是滾動(dòng)條性質(zhì)的了,在滾動(dòng)條下拉的時(shí)候再去呈現(xiàn)當(dāng)前“頁(yè)”的數(shù)據(jù)
2) 即使是一頁(yè)的數(shù)據(jù),也可以一部分一部分呈現(xiàn)
舉個(gè)例子,對(duì)于word文檔的加載一般我們一打開(kāi)就可以看到第一頁(yè),然后滾動(dòng)塊慢慢變小,頁(yè)數(shù)慢慢增多,如果一開(kāi)始就加載1000頁(yè)的話(huà)我們可能要1分鐘后才能看到第一頁(yè),如果等不及直接向后翻滾動(dòng)條的話(huà)會(huì)立即加載后面的數(shù)據(jù):
- new Thread(() =>
- {
- StringBuilder sb = new StringBuilder();
- for (int i = 0; i < 100; i++)
- sb.Append("test");
- string s = sb.ToString();
- for (int i = 0; i < 20; i++)
- {
- BeginInvoke(new Action(() =>
- {
- textBox1.Text += s + i;
- }));
- Thread.Sleep(10);
- }
- }).Start();
設(shè)置文本框允許縱向滾動(dòng)條并且運(yùn)行一下這段程序可以體會(huì)到這個(gè)效果:

原文標(biāo)題:淺談.NET下的多線(xiàn)程和并行計(jì)算(八)Winform多線(xiàn)程編程基礎(chǔ)上
鏈接:http://www.cnblogs.com/lovecindywang/archive/2010/01/06/1640267.html






















