国产一级a片免费看高清,亚洲熟女中文字幕在线视频,黄三级高清在线播放,免费黄色视频在线看

打開APP
userphoto
未登錄

開通VIP,暢享免費(fèi)電子書等14項(xiàng)超值服

開通VIP
WPF - 在子線程中顯示窗口


  記得在剛剛接觸WPF時(shí),我對(duì)它所提供的一個(gè)特性印象尤為深刻:在程序運(yùn)行大規(guī)模計(jì)算時(shí),程序的界面將不會(huì)停止繪制,并能夠在需要進(jìn)行界面的刷新時(shí)進(jìn)行正確的繪制。那么,這種繪制特性是否能在WPF執(zhí)行大規(guī)模計(jì)算時(shí)對(duì)用戶的輸入進(jìn)行響應(yīng)呢?讓我們來做個(gè)試驗(yàn)吧。

  打開示例工程并運(yùn)行,您會(huì)看到控制窗口(Control Window)。點(diǎn)擊Sychronous work所對(duì)應(yīng)的開始鍵,以開始執(zhí)行以下代碼:

1 public void StartSynchronousWork(object sender, RoutedEventArgs e)2 {3     int counter = 0;4     while (counter < 10000000)5     {6         textBlock.Text = counter.ToString();7         counter++;8     }9 }

  上面的代碼中,textBlock是界面中的一個(gè)元素。StartSynchronousWork()函數(shù)的執(zhí)行會(huì)在循環(huán)中不停地對(duì)該界面元素的Text屬性進(jìn)行更新。可是您看到了什么?界面中的相應(yīng)元素并沒有得到刷新,而是在該函數(shù)執(zhí)行完畢后才在界面上反映出該更改。

  這就是我們可能遇到的問題:在一個(gè)需要長(zhǎng)時(shí)間運(yùn)行的代碼中,我們需要及時(shí)地更新當(dāng)前程序運(yùn)行狀態(tài),或在界面中顯示一些信息。但是從上面代碼的運(yùn)行結(jié)果來看,WPF并不能保證這些信息及時(shí)地顯示在界面上。

  當(dāng)然,我在“從Dispatcher.PushFrame()說起”一文中曾經(jīng)提到過Dispatcher.PushFrame()函數(shù)以及經(jīng)過優(yōu)化后的解決方案。但是該方案有眾多缺點(diǎn):發(fā)送消息將不僅僅導(dǎo)致重繪功能的執(zhí)行,更可能導(dǎo)致其它WPF機(jī)制被執(zhí)行;該解決方案需要自行創(chuàng)建一些消息泵,因此軟件開發(fā)人員至少需要在.net代碼中通過PInvoke混入一些Win32函數(shù)調(diào)用。

  而在本文中,我們將提出在多線程中顯示W(wǎng)PF界面這一解決方案。

 

獨(dú)立的UI線程

  在WPF中,我們需要?jiǎng)?chuàng)建一個(gè)獨(dú)立的UI線程以顯示W(wǎng)PF組成。

  首先要考慮的就是界面元素的分割。我們知道,WPF的界面元素基本上都派生于DispatcherObject,因此對(duì)其的使用被限制在創(chuàng)建它的線程之中。又由于WPF的各種計(jì)算常常需要傳遞給子元素,如布局時(shí)使用的Measure-Arrange機(jī)制,因此我們需要保證WPF中的父元素和子元素處于同一線程之中。這也便限制了我們需要將整個(gè)WPF窗口作為最基本的分割單元獨(dú)立地存在于同一線程之中。

  好了。知道如何在多個(gè)線程中分割程序界面以后,我們就需要開始著手在獨(dú)立的線程中顯示窗口了。首先是線程的創(chuàng)建。為了能讓W(xué)PF正確執(zhí)行,我們需要將這個(gè)新線程設(shè)置為STA:

1 Thread newWindowThread = new Thread(new ThreadStart(CreateCounterWindowThread));2 newWindowThread.SetApartmentState(ApartmentState.STA);3 newWindowThread.Start();

  而下面是函數(shù)CreateCounterWindowThread()的實(shí)現(xiàn):

1 private void CreateCounterWindowThread()2 {3     mCounterWindow = new CounterWindow();4     mCounterWindow.ControlWindow = this;5     mCounterWindow.Show();6     mCounterWindow.Closed += (s, e) => 7         mCounterWindow.Dispatcher.BeginInvokeShutdown(DispatcherPriority.Background);8     Dispatcher.Run();9 }

  讓我們來逐行看看這些代碼的意義。該段代碼首先創(chuàng)建了一個(gè)窗口并通過Show()函數(shù)調(diào)用來顯示新創(chuàng)建的窗口。而在該函數(shù)的最后,Dispatcher.Run()函數(shù)將啟動(dòng)一個(gè)消息泵,使當(dāng)前線程擁有了處理消息的能力,從而保證所有發(fā)送到窗口的消息能夠正確地分發(fā)和處理。

  那中間那行對(duì)Closed事件進(jìn)行偵聽并處理的代碼是什么意思呢?我們可以看到,CreateCounterWindowThread()函數(shù)的最后通過Dispatcher.Run()函數(shù)啟動(dòng)了消息泵。該消息泵內(nèi)部會(huì)通過循環(huán)對(duì)消息進(jìn)行處理。在獨(dú)立UI線程中顯示的窗口被關(guān)閉后,我們需要向該消息泵發(fā)送關(guān)閉的通知,以終止消息泵的執(zhí)行。Closed事件的處理函數(shù)中所調(diào)用的BeginInvokeShutdown()函數(shù)實(shí)際上就是在窗口被關(guān)閉后通知Dispatcher終止消息循環(huán),使UI線程的執(zhí)行點(diǎn)從Dispatcher.Run()函數(shù)中跳出,并進(jìn)而釋放UI線程所占用的資源。

 

通訊模型

  OK,您現(xiàn)在已經(jīng)了解了怎樣在獨(dú)立的UI線程中創(chuàng)建窗口實(shí)例。但當(dāng)前的解決方案只能讓您在獨(dú)立的UI線程中創(chuàng)建一個(gè)不與其它線程交互的窗口,而并不能將其它線程中的數(shù)據(jù)顯示在當(dāng)前界面中。當(dāng)然,我會(huì)告訴您該問題的解決方法,那就是Dispatcher的Invoke()和BeginInvoke()函數(shù)。

  在一個(gè)Dispatcher實(shí)例上調(diào)用Invoke()和BeginInvoke()函數(shù)會(huì)向該Dispatcher實(shí)例中放入一個(gè)工作項(xiàng),并在Dispatcher實(shí)例所對(duì)應(yīng)的線程中執(zhí)行該工作項(xiàng)。這兩個(gè)函數(shù)之間的區(qū)別是:Invoke()函數(shù)所插入的工作項(xiàng)會(huì)同步執(zhí)行,而BeginInvoke()函數(shù)所插入的工作項(xiàng)則是異步執(zhí)行。

  對(duì)于本文開頭所提到的問題,我想我們已經(jīng)有了一個(gè)解決方案,那就是在獨(dú)立的UI線程中創(chuàng)建顯示信息的界面,并在主線程需要顯示信息的時(shí)候通過BeginInvoke()函數(shù)將工作項(xiàng)分發(fā)到UI線程中。就如示例代碼中的StartAsynchronousWork()函數(shù)那樣:

 1 public void StartAsynchronousWork(object sender, RoutedEventArgs e) 2 { 3     int counter = 0; 4     while (counter < 10000000) 5     { 6         mCounterWindow.Dispatcher.BeginInvoke( 7             new Action<int>(param =>  8                 { mCounterWindow.textBlock.Text = param.ToString(); }),  9             DispatcherPriority.Background, new object[] { counter });10         counter++;11     }12 }

  我想您腦中可能存在著一些疑問:我們?yōu)槭裁匆肂eginInvoke()而不是Invoke()?為什么我們選擇了DispatcherPriority.Background這一優(yōu)先級(jí),而不是DispatcherPriority.Normal或是更高?對(duì)于第一個(gè)問題,答案是出于性能的考慮。Invoke()函數(shù)會(huì)在調(diào)用時(shí)阻塞當(dāng)前線程的執(zhí)行,而頻繁地調(diào)用該函數(shù)可能導(dǎo)致程序性能的大幅下降。一般情況下,我們會(huì)在線程擁有較大負(fù)荷時(shí)調(diào)用BeginInvoke()函數(shù),以避免Invoke()函數(shù)所造成的開銷。而對(duì)于第二個(gè)問題,則是為了給WPF繪制線程對(duì)界面進(jìn)行繪制的機(jī)會(huì)。眾所周知,WPF單獨(dú)使用一個(gè)繪制線程對(duì)WPF程序的界面進(jìn)行繪制,而該繪制工作的優(yōu)先級(jí)為DispatcherPriority.Render。如果我們一直使用較高的優(yōu)先級(jí)向其發(fā)送工作項(xiàng),那么UI線程也將像文章開始時(shí)所介紹的那種情況一樣不能得到執(zhí)行的機(jī)會(huì)。但是就BeginInvoke()及Invoke()函數(shù)調(diào)用次數(shù)不是很頻繁的情況而言,您也可以選擇使用較高的優(yōu)先級(jí)。

  這只是進(jìn)行單向調(diào)用的情況。但是事情往往不是那么簡(jiǎn)單。您可能常常需要從UI中獲取主線程中的數(shù)據(jù)以輔助顯示。這時(shí)我們需要在Invoke()和BeginInvoke()函數(shù)中使用哪個(gè)呢?一般情況下,我都會(huì)選擇Invoke()函數(shù)。首先,創(chuàng)建獨(dú)立UI線程所要解決的問題常常是主線程不能及時(shí)更新信息,即它常常是負(fù)荷較重的線程。因此出于性能的考慮,我們常常更偏好于在主線程中使用BeginInvoke()將任務(wù)分發(fā)給UI線程。而UI線程則常常是負(fù)載較輕的線程,因此我們可以通過同步調(diào)用Invoke()來從主線程中訪問數(shù)據(jù)。就如示例中的代碼所示:

 1 // ControlWindow.xaml.cs 2 public void StartTimer(object sender, RoutedEventArgs e) 3 { 4     mTimer = new DispatcherTimer(); 5     mTimer.Interval = TimeSpan.FromSeconds(1); 6     mTimer.Tick += new EventHandler(new Action<object, EventArgs>((obj, args) => 7     { 8         mCounterWindow.Dispatcher.BeginInvoke(new Action(mCounterWindow.OnTimerTick)); 9     }));10     mTimer.Start();11 }12 13 public string GetTimeString()14 {15     return DateTime.Now.ToString();16 }17     18 // CounterWindow.xaml.cs19 public void OnTimerTick()20 {21     textBlock.Text = ControlWindow.Dispatcher.Invoke(22         new Func<string>(ControlWindow.GetTimeString)) as string;23 }

  您可能會(huì)繼續(xù)問:那我是否可以在主線程及UI線程中都使用BeginInvoke()函數(shù)?答案是可以,但是那經(jīng)常會(huì)給你帶來不必要的麻煩。下面我們會(huì)從最簡(jiǎn)單的情況說起。

首先您要深刻理解的是,BeginInvoke()函數(shù)是一個(gè)異步調(diào)用,在工作項(xiàng)被執(zhí)行的時(shí)候,我們所需要的數(shù)據(jù)可能已經(jīng)發(fā)生了變化。舉例來說,如果我們?cè)贐eginInvoke()函數(shù)中傳入了一個(gè)字符串參數(shù),并在工作項(xiàng)執(zhí)行時(shí)從主線程中取得取子串時(shí)所需要的索引,那么取子串的操作可能導(dǎo)致致命的錯(cuò)誤:主線程中字符串可能已經(jīng)變更,而索引已經(jīng)超過了原字符串的長(zhǎng)度。正確的方法則是在BeginInvoke()函數(shù)中同時(shí)將字符串以及子串的索引傳入工作項(xiàng),UI線程則僅僅執(zhí)行對(duì)子串的求值即可。

  您可能擔(dān)心:此時(shí)的UI并沒有反映程序執(zhí)行的準(zhǔn)確數(shù)據(jù)。是的,但是所有的軟件都會(huì)在繪制界面時(shí)產(chǎn)生一定的延遲,就好像Windows會(huì)用WM_PAINT消息執(zhí)行繪制。由于在我們所討論的話題中,UI線程并不是一個(gè)擁有較高負(fù)載的線程,因此WPF能保證對(duì)其界面執(zhí)行適時(shí)的刷新。

  在主線程執(zhí)行過程中,我們可以通過BeginInvoke()函數(shù)將具有相同優(yōu)先級(jí)的工作項(xiàng)插入U(xiǎn)I線程的Dispatcher中,這樣可以保證這些工作項(xiàng)按序執(zhí)行。這種按序執(zhí)行在一定程度上類似于我們?cè)谥骶€程中對(duì)界面元素的屬性進(jìn)行設(shè)置,不同的則是,對(duì)界面的繪制可能在屬性設(shè)置到一半的時(shí)候進(jìn)行,即在界面上的數(shù)據(jù)可能分為已更新和未更新兩部分。只是這種不一致會(huì)很快地在下一個(gè)工作項(xiàng)被處理時(shí)即被更新,基本不會(huì)被用戶所察覺。

  當(dāng)然,我們不能忽略UI線程同主線程進(jìn)行溝通的情況。如果這種溝通是由于用戶操作了UI,那么我們需要考慮在UI線程和主線程中分別添加執(zhí)行代碼:UI線程中添加對(duì)UI狀態(tài)的更改,而主線程則用來執(zhí)行業(yè)務(wù)邏輯。當(dāng)然您可以在這里用BeginInvoke(),也就相當(dāng)于向主線程Post了一個(gè)消息。但是從主線程中取得數(shù)據(jù)有時(shí)也是無法避免的,例如界面分支邏輯眾多,無法準(zhǔn)確判斷實(shí)際需要的數(shù)據(jù)這一情況。此時(shí)我們需要使用Invoke()函數(shù)取得需要的數(shù)據(jù)。

  為什么不用異步調(diào)用BeginInvoke()?調(diào)用BeginInvoke()函數(shù)時(shí),您需要偵聽該函數(shù)所返回的DispatcherOperation實(shí)例的Complete事件,并在該事件的響應(yīng)函數(shù)中繼續(xù)執(zhí)行邏輯。這樣做至少有兩個(gè)缺點(diǎn):代碼被分割到了多個(gè)函數(shù)中,難以閱讀和維護(hù);BeginInvoke()函數(shù)返回時(shí),UI線程可能又執(zhí)行了其它BeginInvoke()函數(shù)所插入的工作項(xiàng),從而使UI狀態(tài)和函數(shù)調(diào)用時(shí)所使用的數(shù)據(jù)不一致。

 

Dispatcher的內(nèi)部實(shí)現(xiàn)

  雙向通訊的模型中,我們已經(jīng)基本否定了兩個(gè)方向都使用BeginInvoke()的情況。那么兩個(gè)方向都使用Invoke()呢?在示例程序中,我已經(jīng)給出了實(shí)驗(yàn)代碼。您只需將StartTimer()函數(shù)中的BeginInvoke()函數(shù)更改為Invoke()函數(shù)調(diào)用即可:

 1 // ControlWindow.xaml.cs 2 public void StartTimer(object sender, RoutedEventArgs e) 3 { 4     mTimer = new DispatcherTimer(); 5     mTimer.Interval = TimeSpan.FromSeconds(1); 6     mTimer.Tick += new EventHandler(new Action<object, EventArgs>((obj, args) => 7     { 8         mCounterWindow.Dispatcher.BeginInvoke(new Action(mCounterWindow.OnTimerTick)); 9     }));10     mTimer.Start();11 }

  接下來,您就可以看到結(jié)果:程序死鎖了。為什么?

  我喜歡在產(chǎn)生疑問的時(shí)候看看WPF實(shí)現(xiàn)源碼,一來可以很清晰地了解產(chǎn)生問題的原因,二來可以從這些源碼中學(xué)到一些新的東西。經(jīng)過擴(kuò)展及整理后的Invoke()函數(shù)的源碼如下:

 1 internal object Invoke(…) 2 { 3 4 DispatcherOperation operation = BeginInvokeImpl(priority, method,  5     args, isSingleParameter); 6     if (operation != null) 7     { 8         operation.Wait(timeout); 910     }11     return null;12 }

  哦,這里透露了兩個(gè)重要的信息:Invoke()函數(shù)的內(nèi)部實(shí)現(xiàn)實(shí)際上調(diào)用了BeginInvoke(),并暫停當(dāng)前線程的執(zhí)行,直到BeginInvoke()函數(shù)返回。這也便能解釋前面線程死鎖的原因了:在一個(gè)線程通過Invoke()函數(shù)調(diào)用另一個(gè)線程的時(shí)候,自身便進(jìn)入休眠狀態(tài)。如果被調(diào)用線程反過來想調(diào)用調(diào)用者,則會(huì)因?yàn)樵摼€程已經(jīng)處于休眠狀態(tài)而不能進(jìn)行響應(yīng)。

  接下來我們看看BeginInvokeImp()的實(shí)現(xiàn)吧,也就是BeginInvoke()的實(shí)際實(shí)現(xiàn):

 1 internal DispatcherOperation BeginInvokeImpl(…) 2 { 3 4     DispatcherHooks hooks = null; 5     bool flag = false; 6     lock (this._instanceLock) 7     { 8         DispatcherOperation data = new DispatcherOperation(…) { 9             _item = this._queue.Enqueue(priority, data)10         };11         // this.RequestProcessing() 內(nèi)部實(shí)際調(diào)用了TryPostMessage()函數(shù)12 MS.Win32.UnsafeNativeMethods.TryPostMessage(new HandleRef(this, 13 this._window.Value.Handle), _msgProcessQueue, IntPtr.Zero, IntPtr.Zero);1415     }1617 }

  上面的代碼首先將需要執(zhí)行的工作項(xiàng)插入到表示消息隊(duì)列的成員_queue中。接下來,WPF通過Win32調(diào)用TryPostMessage()發(fā)送一個(gè)_msgProcessQueue消息。也就是說,我們對(duì)Invoke()及BeginInvoke()函數(shù)的調(diào)用僅僅相當(dāng)于發(fā)送了一個(gè)消息,只是Invoke()函數(shù)會(huì)暫停當(dāng)前線程的執(zhí)行以等待該消息被處理完畢而已。而在_msgProcessQueue消息的處理函數(shù)ProcessMessage()函數(shù)中,工作項(xiàng)才被真正執(zhí)行:

 1 // 整理后的ProcessQueue()函數(shù) 2 private void ProcessQueue() 3 { 4     DispatcherOperation operation = null; 5     lock (this._instanceLock) 6     { 7         DispatcherPriority invalid = this._queue.MaxPriority; 8         if (((invalid != DispatcherPriority.Invalid)  9             && (invalid != DispatcherPriority.Inactive)) 10             && _foregroundPriorityRange.Contains(invalid))11             operation = this._queue.Dequeue();12         this.RequestProcessing(); // 內(nèi)部再次發(fā)出_msgProcessQueue消息13     }14     if (operation != null)15         operation.Invoke(); // 執(zhí)行工作項(xiàng)16 }

 

寫在后面

  本文介紹了創(chuàng)建獨(dú)立的UI線程以顯示W(wǎng)PF窗口的一種方法。但在使用該方法的之前,您首先需要研究一下我們是否真的需要使用它。按照一般的軟件設(shè)計(jì)思路,耗時(shí)的工作都需要置于后臺(tái)線程中。

  其次注意線程之間的忙碌情況。這是決定到底哪個(gè)線程調(diào)用BeginInvoke()函數(shù)而哪個(gè)線程調(diào)用Invoke()函數(shù)的最重要依據(jù):如果一個(gè)線程較為忙碌,那么對(duì)BeginInvoke()函數(shù)的響應(yīng)將會(huì)較為緩慢。所以在一般情況下,具有較低負(fù)載的線程用來接收BeginInvoke()函數(shù),以保證程序能及時(shí)地對(duì)用戶的輸入進(jìn)行響應(yīng)。

  最后要強(qiáng)調(diào)的是,在該實(shí)現(xiàn)中不能同時(shí)用兩個(gè)Invoke()函數(shù)。這是因?yàn)橄裆厦嫠榻B的那樣,對(duì)Invoke()函數(shù)的調(diào)用會(huì)阻止當(dāng)前線程的執(zhí)行。那么在另一個(gè)線程調(diào)用Invoke()函數(shù)請(qǐng)求當(dāng)前線程執(zhí)行工作項(xiàng)時(shí),將因?yàn)楫?dāng)前線程已經(jīng)被阻止,從而無法得到當(dāng)前線程的響應(yīng),并最終死鎖。

 

本站僅提供存儲(chǔ)服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊舉報(bào)。
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
WPF線程模型及委托事件應(yīng)用
詳解WPF線程模型和Dispatcher
WPF的線程模型
C# this.Invoke()的作用與用法
WPF中Dispatcher的初步探討
WPF 基礎(chǔ)到企業(yè)應(yīng)用系列3——Dispatcher及多線程
更多類似文章 >>
生活服務(wù)
分享 收藏 導(dǎo)長(zhǎng)圖 關(guān)注 下載文章
綁定賬號(hào)成功
后續(xù)可登錄賬號(hào)暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點(diǎn)擊這里聯(lián)系客服!

聯(lián)系客服