在.NET客戶端程序中使用多線程(二)

最簡單的線程同步

在本欄目開始我就稱保持線程同步而不互相沖突是一門藝術。Figure 3 所示的FlawedMultiThreadForm.cs應用程序有一個問題:用戶可以通過單擊按鈕引發一個很長的響鈴操作,他們可以繼續單擊按鈕而引發更多的響鈴操作。如果不是響鈴,該長操作是數據庫查詢或者在進程的內存中進行數據結構操作,你一定不想在同一時間內,有一個以上的線程做同樣的工作。最好的情況下這是系統資源的一種浪費,最壞的情況下會導致數據毀滅。

最容易的解決辦法就是禁止按鈕一類的用戶交互元素;兩個進程間的通信稍微有點難度。過一會我將給你看如何做這些事情。但首先,讓我指出所有線程同步使用的一些線程間通信的形式-從一個線程到另一個線程通信的一種手段。稍後我將討論大家所熟知的AutoResetEvent對象類型,它僅用在線程間通信。

現在讓我們首先看一下爲Figure 3 中FlawedMultiThreadedForm.cs程序中加入的線程同步代碼。再一次的,Figure 4 CorrectMultiThreadedForm.cs程序中紅色部分表示的是其先前程序的較小的改動部分。 如果你運行這個程序你將看到當一個長響鈴操作在進行時用戶交互被禁止了(但沒有掛起),響鈴完成的時候又被允許了。這次這些代碼的變化已經足夠了,我將逐個運行他們。

Figure 4 CorrectMultiThreadedForm.cs

using System;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;
using Microsoft.VisualBasic;

class App {

// Application entry point
public static void Main() {

// Run a Windows Forms message loop
Application.Run(new CorrectMultiThreadedForm());
}
}

// A Form-derived type
class CorrectMultiThreadedForm : Form{

// Constructor method
public CorrectMultiThreadedForm() {

// Create a textbox
text.Location = new Point(10, 10);
text.Size = new Size(50, 20);
Controls.Add(text);

// Create a button
button.Text = "Beep";
button.Size = new Size(50, 20);
button.Location = new Point(80, 10);

// Register Click event handler
button.Click += new EventHandler(OnClick);

Controls.Add(button);

// Cache a delegate for repeated reuse
enableControls = new BooleanCallback(EnableControls);
}

// Method called by the button's Click event
void OnClick(Object sender, EventArgs args) {
// Get an int from a string
Int32 count = 0;
try { count = Int32.Parse(text.Text); } catch (FormatException) {}

// Count to that number
EnableControls(false);
WaitCallback async = new WaitCallback(Count);
ThreadPool.QueueUserWorkItem(async, count);
}

// Async method beeps once per second
void Count(Object param) {

Int32 seconds = (Int32) param;
for (Int32 index = 0; index < seconds; index++) {
Interaction.Beep();
Thread.Sleep(1000);
}

Invoke(enableControls, new Object[]{true});
}

void EnableControls(Boolean enable) {
button.Enabled = enable;
text.Enabled = enable;
}

// A delegate type and matching field
delegate void BooleanCallback(Boolean enable);
BooleanCallback enableControls;

// Some private fields by which to reference controls
Button button = new Button();
TextBox text = new TextBox();
}

在Figure 4 的末尾處有一個EnableControls的新方法,它允許或禁止窗體上的文本框和按鈕控件。在Figure 4 的開始我加入了一個EnableControls調用,在後臺響鈴操作排隊等候之前立即禁止文本框和按鈕。到這裏線程的同步工作已經完成了一半,因爲禁止了用戶交互,所以用戶不能引發更多的後臺衝突操作。在Figure 4 的末尾你將看到一個名爲BooleanCallback的委託類型被定義,其簽名是同EnableControls方法兼容的。在那個定義之前,一個名爲EnableControls的委託域被定義(見例子),它引用了該窗體的EnableControls方法。這個委託域在代碼的開始處被分配。

你也將看到一個來自主線程的回調,該主線程爲窗體和其控件擁有和提取消息。這個調用通過向EnableControls傳遞一個true參數來使能控件。這通過後臺線程調用窗體的Invoke方法來完成,當其一旦完成其長響鈴操時。代碼傳送的委託引用EnableControls去Invoke,該方法的參數帶有一個對象數組。Invoke方法是線程間通信的一個非常靈活的方式,特別是對於Windows Forms類庫中的窗口或窗體。在這個例子中,Invoke被用來告訴主GUI線程通過調用EnableControls方法重新使能窗體上的控件。

Figure 4 中的CorrectMultiThreadedForm.cs的變化實現了我早先的建議――當響鈴操作在執行時你不想運行,就禁止引發響鈴操作的用戶交互部分。當操作完成時,告訴主線程重新使能被禁止的部分。對Invoke的調用是唯一的,這一點應該注意。

Invoke方法在 System.Windows.Forms.Controls類型中定義,包含Form類型讓類庫中的所有派生控件都可使用該方法。Invoke方法的目的是配置了一個從任何線程對爲窗體或控件實現消息提取線程的調用。

當訪問控件派生類時,包括Form類,從提取控件消息的線程來看你必須這樣做。這在單線程的應用程序中是很自然的事情。但是當你從線程池中使用多線程時,要避免從後臺線程中調用用戶交互對象的方法和屬性是很重要的。相反,你必須使用控件的Invoke方法間接的訪問它們。Invoke是控件中很少見的一個可以安全的從任何線程中調用的方法,因爲它是用Win32的PostMessage API實現的。

使用Control.Invoke方法進行線程間的通信有點複雜。但是一旦你熟悉了這個過程,你就有了在你的客戶端程序中實現多線程目標的工具。本欄目的剩餘部分將覆蓋其它一些細節,但是Figure 4 中的CorrectMultiThreadedForm.cs應用程序是一個完整的解決辦法:當執行任意長的操作時仍然能夠響應用戶的其它操作。儘管大多數的用戶交互被禁止,但用戶仍然可以重新配置和調整窗口,也可以關閉程序。然而,用戶不能任意使用程序的異步行爲。這個小細節能夠讓你對你的程序保持自信心。

在我的第一個線程同步程序中,沒有使用任何傳統的線程結構,例如互斥或信號量,似乎一錢不值。然而,我卻使用了禁止控件的最普通的方法。

細節-實現一個取消按鈕

有時你想爲你的用戶提供一種取消長操作的方法。你所需要的就是你的主線程同後臺線程之間的一些通信方法,通知後臺線程操作不再被需要,可以停止。System.Threading名字空間爲這個方法提供了一個類:AutoResetEvent。

AutoResetEvent是線程間通信的一種簡單機制。一個AutoResetEvent對象可以有兩種狀態中的一個:有信號的和無信號的。當你創建一個AutoResetEvent實例時,你可以通過構造函數的參數來決定其初始狀態。然後感知該對象的線程通過檢查AutoResetEvent對象的狀態,或者用 AutoResetEvent對象的Set或Reset方法調整其狀態,進行相互通信。

在某種程度上AutoResetEvent很像一個布爾類型,但是它提供的特徵使其更適合於在線程間進行通信。這樣的一個例子就是它有這種能力:一個線程可以有效的等待直到一個AutoResetEvent對象從一個無信號的狀態變爲有信號的狀態。它是通過在該對象上調用WaitOne實現的。任何一個線程對一個無信號的AutoResetEvent對象調用了WaitOne,就會被有效的阻塞直到其它線程使該對象有信號。使用布爾變量線程必須在一個循環中登記該變量,這是無效率的。一般來說沒有必要使用Reset來使一個AutoResetEvent變爲無信號,因爲當其它線程感知到該對象爲有信號時,它會被立即自動的設爲無信號的。

現在你需要一種讓你的後臺線程無阻塞的測試AutoResetEvent對象的方法,你會有許多工具實現線程的取消。爲了完成這些,調用帶有WaitOne的重載窗體並指出一個零毫秒的超出時間,以零毫秒爲超出時間的WaitOne會立即返回,而不管AutoResetEvent對象的狀態是否爲有信號。如果返回值爲true,這個對象是有信號的;否則由於時間超出而返回。

我們整理一下實現取消的特點。如果你想實現一個取消按鈕,它能夠取消後臺線程中的一個長操作,按照以下步驟:

在你的窗體上加入AutoResetEvent域類型

通過在AutoResetEvent的構造函數中傳入false參數,設置該對象初始狀態爲無信號的。 接着在你的窗體上保 存該對象的引用域,這是爲了能夠在窗體的整個生命週期內可以對後臺線程的後臺操作實現取消操作。

在你窗體上加入一個取消按鈕。

在取消按鈕的Click事件處理器中,通過調用AutoResetEvent對象的Set方法使其有信號。

同時,在你的後臺線程的邏輯中週期性地在AutoResetEvent對象上調用WaitOne來檢查用戶是否取消了。

if(cancelEvent.WaitOne(0, false)){
// cancel operation
}

你必須記住使用零毫秒參數,這樣可以避免在後臺線程操作中不必要的停頓。

如果用戶取消了操作,通過主線程AutoResetEvent會被設爲有信號的。 當WaitOne返回true時你的後臺線程會 得到警告,並停止操作。同時在後臺線程中由於調用了WaitOne該事件會被自動的置爲無信號狀態。

爲了能夠看到取消長操作窗體的例子,你可以下載CancelableForm.cs文件。這個代碼是一個完整的程序,它與Figure 4 中的CorrectMultiThreadedForm.cs只有稍微的不同。

注意在CancelableForm.cs也採用了比較高級的用法Control.Invoke, 在那裏EnableControls方法被設計用來調用它自己如果當它被一個錯誤的線程所調用時。在它使用窗體上的任何GUI對象的方法或屬性時要先做這個檢查。 這樣能夠使得EnableControls能夠從任何線程中直接安全的調用,在方法的實現中有效的隱藏了Invoke調用的複雜性。這些可以使應用程序更加有維護性。注意在這個例子中同樣使用了Control.BeginInvoke, 它是Control.Invoke的異步版本。

你也許注意到取消的邏輯依賴於後臺線程通過WaitOne調用週期性的取消檢查的能力。 但是如果正在討論的問題不能被取消怎麼辦?如果後臺操作是一個單個調用,像DataAdapter.Fill,它會花很長時間?有時會有解決辦法的,但並不總是。

如果你的長操作根本不能取消,你可以使用一個僞取消的方法來完成你的操作,但在你的程序中不要影響你的操作結果。這不是技術上的取消操作,它把一個可忍受的操作幫定到一個線程池中,但這是在某種情況下的一種折中辦法。如果你實現了類似的解決辦法,你應該從你的取消按鈕事件處理器中直接使能你已禁止的UI元素,而不要還依賴於被綁定的後臺線程通過Invoke調用使能你的控件。同樣重要的使設計你的後臺操作線程,當其返回時測試一下它是否被取消,以便它不影響現在被取消的操作的結果。

這種長操作取消是比較高級的方法,它只在某些情況下才可行。例如,數據庫查詢的僞取消就是這樣,但是一個數據庫的更新,刪除,插入僞取消是一個滯後的操作。有永久的操作結果或與反饋有關的操作,像聲音和圖像,就不容易使用僞取消方法,因爲操作的結果在用戶取消以後是非常明顯的。

更多細節-有關定時器

在應用程序中需要一個定時器來引發一個定期的任務一定不一般。例如,如果你的程序在窗體的狀態條上顯示當前時間,你可能每5秒鐘更新一次時間。System.Threading 名字空間包括了一個名爲Timer多線程定時器類。

當你創建一個定時器類的實例時,你爲定時器回調指明瞭一個以毫秒爲單位的週期,而且你也傳遞給該對象一個委託用來每過一個時鐘週期調用你。回調發生在線程池中的線程上。事實上,每次時鐘週期到來時真正發生的是一個工作條目在線程池中排隊;一般來說一個調用會馬上發生的,但是如果線程池比較忙,這個回調也許會在稍後的一個時間點發生。

如果你考慮在你的程序中使用多線程,你也許會考慮使用定時器類。然而,如果你的程序使用了Windows窗體,你不必使用多線程的行爲,在System.Windows.Forms名字空間中有另外一個也叫Timer的定時器類。

System.Windows.Forms.Timer與其多線程的同伴比起來有一個明顯的好處:因爲它不是多線程的,所以不會在其它線程中對你進行回調,而且更適合爲應用程序提取窗口消息的主線程。實際上System.Windows.Forms.Timer的實現是在系統中使用了WM_TIMER的一個窗口消息。這種方法在你的System.Windows.Forms.Timer的事件處理器中不必擔心線程同步,線程間通信之類的問題。

對於Windows窗體類程序,作爲一個很好的技巧就是使用System.Windows.Forms.Timer類, 除非你特別需要線程池中的線程對你進行回調。既然這種要求很少見,爲了使事情簡單,把使用System.Windows.Forms.Timer作爲一個規則,即使在你的程序的其它地方使用了多線程。

展望將來

微軟最近展示了一個即將出現的GUI API,代號爲“Avalon”,本期MSDN雜誌的問題列表中(見70頁)Charles Petzold''''s的文章描述了其特點。在Avalon框架中用戶接口元素沒有被系與一個特殊的線程;作爲更換每個用戶接口元素與一個單獨的邏輯線程上下文相關聯,在UIContext類中實現。但是當你發現UIContext類中包含了Invoke方法,及其姊妹BeginInvoke時,你就不會驚奇了,在名字上與窗體類中的控件類上名稱一樣的目的是說明他們在邏輯作用上是一致的。

發佈了16 篇原創文章 · 獲贊 1 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章