多線程操作之窗體控件
ⅠWindows 窗體控件的線程安全性和InvokeRequired屬性
Windows 窗體中的控件被綁定到特定的線程,不具備線程安全性。這就是說當我們企圖從一個線程中操作在另一個線程中創建的控件時,可能會產生意想不到的錯誤。爲此多線程環境下操作窗體控件時必須注意要使用那些線程安全的方法、成員或事件。
多數控件從Windows基類(比如Control類)那裏繼承並公開了InvokeRequired 屬性。當創建控件句柄的線程和調用線程不同時,該屬性指示調用方在調用控件的方法時是否必須調用 Invoke 方法。除InvokeRequired 屬性以外,控件上還有以下四個線程安全的方法可供調用:Invoke、BeginInvoke、EndInvoke 和 CreateGraphics。當從另一個線程對除此之外的所有其他成員的進行調用時,應使用這些 Invoke 方法中的一個。
雖然InvokeRequired 屬性是線程安全的,但是在使用它的時候仍然需要小心。InvokeRequired 屬性爲False時,有可能它意味着我們可以不使用Invoke(調用發生在同一線程上),但那只是可能。我們需要知道,對於句柄尚未建立的控件, InvokeRequired 會沿控件的父級鏈搜索,直到它找到有窗口句柄的控件或窗體爲止。如果找不到合適的句柄,InvokeRequired 方法將返回 false。 這時候調用控件上屬性、方法或事件就很可能導致在後臺線程上創建控件的句柄,從而隔離不帶消息泵的線程上的控件並使應用程序不穩定。
通常來說,這種問題僅會發生在下面所說的情況中。那就是我們在應用程序主窗體的構造函數中創建了後臺線程(如同在 Application.Run(new MainForm()) 中),並試圖在窗體已經顯示或取消 Application.Run 之前啓動該線程。 因爲我們容易忽略一個事實,那就是在啓動窗體的Load 事件之前,除非存在強制操作,否則窗體和窗體控件的句柄是不會被創建的。
爲了避免上述問題,當 InvokeRequired 在後臺線程上返回 false 時,我們往往需要檢查 IsHandleCreated 的值來確定控件的句柄是否被創建。如果尚未創建,那麼我們必須等待窗體句柄建立(可以通過調用 Handle 屬性強制創建句柄、或者等待窗體的 Load 事件)後去才啓動後臺進程。 更優的選擇是使用 SynchronizationContext 返回的 SynchronizationContext,而不是使用控件進行線程間封送處理。
Ⅱ示例:MultiRow多線程加載數據
首先,我們爲多線程操作MultiRow提供一個邏輯控制和線程管理的類,本例儘量簡單以突出顯示我們是怎樣使用InvokeRequired 屬性和Invoke方法的。程序中我們使用了MultiRow父窗體的Invoke方法而不是MultiRow自己的Invoke方法。
/// 簡單的數據加載控制
/// </summary>
public class DataLoadManager
{
/// <summary>
/// 定義結構體,用於保存數據開始和結束行
/// </summary>
struct dataIndexForThread
{
public int StartRow ;
public int EndRow;
}
//創建兩個線程
Thread t1;
Thread t2;
//開始和結束控制
dataIndexForThread rowsInThread1;
dataIndexForThread rowsInThread2;
//記錄已經完成的線程數
int completedThread = 0;
//控件和控件父窗體
GrapeCity.Win.ElTabelle.MultiRowSheet _multirow;
Form _Parent;
//提供事件以加載數據
public delegate void loadData(int rowIndex);
public event loadData LoadDataToMultiRow;
//提供事件以在數據加載前後作客戶處理
public delegate void loadOver();
public event loadOver LoadDataIsCompleted;
public event loadOver LoadDataIsBegin;
public DataLoadManager(int MaxRows)
{
rowsInThread1.StartRow = 0;
rowsInThread1.EndRow = MaxRows / 2;
rowsInThread2.StartRow = rowsInThread1.EndRow;
rowsInThread2.EndRow = MaxRows;
}
/// <summary>
/// 爲加載指定行範圍內的數據提供邏輯控制
/// </summary>
/// <param name="rows">該進程內處理的行範圍</param>
private void loadDataInSeparatedThread(object rows)
{
dataIndexForThread rowsInThread = (dataIndexForThread)rows;
if (_multirow.InvokeRequired)
{
_Parent.Invoke(new WaitCallback(loadDataInSeparatedThread), new object[] { rows });
}
else
{
for (int i = rowsInThread.StartRow; i < rowsInThread.EndRow; i++)
{
if (LoadDataToMultiRow != null)
LoadDataToMultiRow(i);
}
completedThread += 1;
if (completedThread == 2 && LoadDataIsCompleted != null)
{
LoadDataIsCompleted();
}
}
}
/// <summary>
/// 啓動多線程
/// </summary>
/// <param name="Parent"></param>
/// <param name="multirow"></param>
public void Start(Form Parent, GrapeCity.Win.ElTabelle.MultiRowSheet multirow)
{
_multirow = multirow;
_Parent = Parent;
if (LoadDataIsBegin != null) LoadDataIsBegin();
t1 = new Thread(new ParameterizedThreadStart(loadDataInSeparatedThread));
t2 = new Thread(new ParameterizedThreadStart(loadDataInSeparatedThread));
t1.IsBackground = true;
t1.Start(rowsInThread1);
t2.IsBackground = true;
t2.Start(rowsInThread2);
}
}
以下代碼用來驗證多線程MultiRow加載數據。該代碼架設你擁有一個WinForm窗口,該窗口上添加有一個MultiRow控件和一個Button控件。該程序需要引用System.Threading命名空間。
DataLoadManager dataManager = new DataLoadManager(MaxRows);
dataManager.LoadDataIsBegin += new DataLoadManager.loadOver(setDataToMultiRowBegin);
dataManager.LoadDataToMultiRow += new DataLoadManager.loadData(setDataToMultiRow);
dataManager.LoadDataIsCompleted += new DataLoadManager.loadOver(setDataToMultiRowCompleted);
dataManager.Start(this, multiRowSheet0);
//在MultiRow的窗口內添加以下方法
private void setDataToMultiRowBegin(
{
multiRowSheet0.BeginUpdate();
multiRowSheet0.MaxMRows = 0;
multiRowSheet0.MaxMRows = MaxRows;
}
private void setDataToMultiRowCompleted()
{
multiRowSheet0.EndUpdate();
}
private void setDataToMultiRow(int row)
{
multiRowSheet0[row, "列名"].Text = "該行該列的取值";
}
Ⅲ總結
在本示例中,我們發現多線程加載數據比通常的加載方式耗費了更多的時間,而應用多線程的目的卻往往正是爲了節省時間。求其原因,是本例應用的場合不對。多線程適用於耗時計算等場合。比如說本例中在數據加載的同時需要大量計算的話,那麼爲了增強用戶體驗使其不至長時間等待,那麼可以考慮使用多線程。很多時候甚至可以先加載顯示那些容易得到的數據。而在後臺用多線程並行計算那些耗時數據然後「默默的」填充到MultiRow上。
更重要的是本例存在嚴重的安全隱患,它無法應對我們第一節中提出的那些問題。比如說,我們想向窗口動態添加一個MultiRow,它加載完數據之後添加顯示到窗體上。那麼如果在主線程調用Controls.Add()方法將控件加載到窗口之前,我們無意間在後臺進程中創建了控件句柄,就將可能產生以下幾種錯誤:創建句柄的進程完成工作而結束,其他後臺進程將無法繼續爲MultiRow加載數據,系統提示先行建立的線程不存在;進程順利完成數據加載,但是主進程執行Controls.Add()方法報錯,程序因此無法將該MultiRow添加到界面。而且這種錯誤往往很細微難於發現。至於具體的測試代碼這裏就不在贅述。解決的手段也第一節中已經交待,具體實現因需求而異。但是一般來說存在一個基本準則,除了線程獨自使用的控件,不要在後臺線程中創建空間句柄,全局控件句柄的持有者必須是伴隨程序進程始終的線程。