轉帖|其它|編輯:郝浩|2011-04-11 13:52:45.000|閱讀 653 次
概述:現在我們已經了解,EndInvoke可以給我們提供傳出參數與更新后的ref參數;也可以向我們導出異步函數中的異常信息。例如,我們使用 BeginInvoke調用了異步函數Sleep,它開始執行。之后調用EndInvoke,可以獲取Sleep何時執行完成。但如果我們在Sleep執行完成20分鐘后,才去調用EndInvoke呢?EndInvoke仍然會給我們提供傳出值及異步中的異常(假如產生了異常),那么這些信息到底存儲在哪里?EndInvoke如何在函數執行如此久之后仍然能夠調用這些返回值?
# 界面/圖表報表/文檔/IDE等千款熱門軟控件火熱銷售中 >>
了解IAsyncResult
現在我們已經了解,EndInvoke可以給我們提供傳出參數與更新后的ref參數;也可以向我們導出異步函數中的異常信息。例如,我們使用BeginInvoke調用了異步函數Sleep,它開始執行。之后調用EndInvoke,可以獲取Sleep何時執行完成。但如果我們在Sleep執行完成20分鐘后,才去調用EndInvoke呢?EndInvoke仍然會給我們提供傳出值及異步中的異常(假如產生了異常),那么這些信息到底存儲在哪里?EndInvoke如何在函數執行如此久之后仍然能夠調用這些返回值?答案就在于IAsyncResult對象。EndInvoke每次在執行后,都會調用一個該對象作為參數,它包括以下信息:
● 異步函數是否已經完成
● 對調用了BeginInvoke方法的委托的引用
● 所有的傳出參數及它們的值
● 所有的ref參數及它們的更新值
● 函數的返回值
● 異步函數產生的異常
IAsyncResult看起來空無一物,這是因為它僅僅是一個包含了若干屬性的接口;而實際上,它是一個System.Runtime.Remoting.Messaging.AsyncResult對象。
如果我們在編譯器運行期間監視tag的狀態,就會發現,AsyncResult對象下包含類型為System.Runtime.Remoting.Messaging.ReturnMessage的對象。點開它,就會發現這個標簽中包含的所有的異步函數的執行信息!
使用Callback委托:好萊塢原則”不要聯系我,我會聯系你”
目前為止,我們需要了解如何傳遞參數、如何捕捉異常;了解我們的異步方法其實是執行在線程池中的某個具體線程對象中。唯一未涉及到的就是如何在異步函數執行完成后得到通知。畢竟,阻塞調用線程等待函數結束的做法始終差強人意。為了實現這個目的,我們必須為BeginInvoke函數提供一個Callback委托。觀察一下兩個函數:
private void CallSleepWithoutOutAndRefParameterWithCallback()
{
// 創建幾個參數
string strParam = "Param1";
int intValue = 100;
ArrayList list = new ArrayList();
list.Add("Item1");
// 創建委托對象
DelegateWithParameters delSleep =
new DelegateWithParameters(FuncWithParameters);
delSleep.BeginInvoke(out intValue, strParam, ref list, new AsyncCallback(CallBack), null);
}
private void CallBack(IAsyncResult tag)
{
// 我們的int參數標記了out,因此此處不能定義初始值
int intOutputValue;
ArrayList list = null;
// IAsyncResult實際上就是AsyncResult對象,
// 取得它也就可以從中取得用于調用函數的委托對象
AsyncResult result = (AsyncResult)tag;
// 取得委托
DelegateWithParameters del = (DelegateWithParameters)result.AsyncDelegate;
// 取得委托后,我們需要在其上執行EndInvoke。
// 這樣就可以取得函數中的執行結果。
string strReturnValue = del.EndInvoke(out intOutputValue, ref list, tag);
Trace.WriteLine(strReturnValue);
}
在這里,我們向BeginInvoke傳遞了Callback回調函數。這樣.NET就可以在FuncWithParameters()執行完后調用Callback函數。在之前,我們已經了解到,必須使用EndInvoke來取得函數的執行結果,注意上面為了使用EndInvoke,我們使用了一些特殊操作來取得delegate對象。
// IAsyncResult實際上就是AsyncResult對象,
// 取得它也就可以從中取得用于調用函數的委托對象
AsyncResult result = (AsyncResult)tag;
// 取得委托
DelegateWithParameters del = (DelegateWithParameters)result.AsyncDelegate;
最后一個問題:回調函數執行在什么線程?
總而言之,Callback函數(回調函數)是.NET通過我們的委托對象來實現調用的。我們可能會希望得到一個更清晰的畫面:回調函數究竟執行在那個線程?為了達到這個目的:我們在函數中加入線程日志。
private string FuncWithParameters(out int param1, string param2, ref ArrayList param3)
{
// 記錄線程信息
Trace.WriteLine("In FuncWithParameters: Thread Pool? "
+ Thread.CurrentThread.IsThreadPoolThread.ToString() +
" Thread Id: " + Thread.CurrentThread.GetHashCode());
// 掛起秒以模擬線程在這里執行了耗時較長的任務
Thread.Sleep(4000);
// 我們在這里改變參數值
param1 = 300;
param2 = "hello";
param3 = new ArrayList();
// 這里執行一些耗時較長的工作
Thread.Sleep(3000);
return "thank you for reading me";
}
private void CallBack(IAsyncResult tag)
{
// 回調函數在什么線程執行?
Trace.WriteLine("In Callback: Thread Pool? "
+ Thread.CurrentThread.IsThreadPoolThread.ToString() +
" Thread Id: " + Thread.CurrentThread.GetHashCode());
// 我們的int參數標記了out,因此此處不能定義初始值
int intOutputValue;
ArrayList list = null;
// IAsyncResult實際上就是AsyncResult對象,
// 取得它也就可以從中取得用于調用函數的委托對象
AsyncResult result = (AsyncResult)tag;
// 取得委托
DelegateWithParameters del = (DelegateWithParameters)result.AsyncDelegate;
// 取得委托后,我們需要在其上執行EndInvoke。
// 這樣就可以取得函數中的執行結果。
string strReturnValue = del.EndInvoke(out intOutputValue, ref list, tag);
Trace.WriteLine(strReturnValue);
}
我將CallSleepWithoutOutAndRefParameterWithCallback()函數放在某個窗體按鈕的單擊事件中,并且連續點擊三次,將得到這樣的執行結果:
注意FuncWithParameter函數被連續執行了3次,它們依次被執行在相互獨立的線程上,并且這些線程來自于線程池。而他們各自的回調函數也執行在與FuncWithParameter相同的線程中。線程11執行了FuncWithParameter,3秒后,它的回調函數也執行在線程11中,線程12、13也是同樣。這樣,我們可以認為回調函數實際上是異步函數的一種延續。
為什么要這樣做?也許是因為這樣我們就不必過多的耗費線程池中的線程,達到線程復用的效果;通過執行在相同的線程,也可以避免不同的線程間傳遞上下文環境帶來的損耗問題。
到此為止,我們在Form中執行異步函數,將會得到一個完全不堵塞主線程的異步調用,這就是我們所希望的效果!
應用場景模擬
現在我們了解了BeginInvoke、EndInvoke、Callback的使用及特點,如何將他們運用到我們的Win Form程序中,使數據的獲取不再阻塞UI線程,實現異步加載數據的效果?我們現在通過一個具體實例來加以說明。
場景描述:將系統的操作日志從數據庫中查詢出來,并且加載到前端的ListBox控件中。
要求:查詢數據庫的過程是個時間復雜度較高的作業,但我們的窗體在執行查詢時,不允許出現”假死”的情況。
private void button1_Click(object sender, EventArgs e)
{
GetLogDelegate getLogDel = new GetLogDelegate(GetLogs);
getLogDel.BeginInvoke(new AsyncCallback(LogTableCallBack), null);
}
public delegate DataTable GetLogDelegate();
/// <summary>
/// 從數據庫中獲取操作日志,該操作耗費時間較長,
/// 且返回數據量較大,日志記錄可能超過萬條。
/// </summary>
/// <returns></returns>
private DataTable GetLogs()
{
string sql = "select * from ***";
DataSet ds = new DataSet();
using (OracleConnection cn = new OracleConnection(connectionString))
{
cn.Open();
OracleCommand cmd = new OracleCommand(sql, cn);
OracleDataAdapter adapter = new OracleDataAdapter(cmd);
adapter.Fill(ds);
}
return ds.Tables[0];
}
/// <summary>
/// 綁定日志到ListBox控件。
/// </summary>
/// <param name="tag"></param>
private void LogTableCallBack(IAsyncResult tag)
{
AsyncResult result = (AsyncResult)tag;
GetLogDelegate del = (GetLogDelegate)result.AsyncDelegate;
DataTable logTable = del.EndInvoke(tag);
if (this.listBox1.InvokeRequired)
{
this.listBox1.Invoke(new MethodInvoker(delegate()
{
BindLog(logTable);
}));
}
else
{
BindLog(logTable);
}
}
private void BindLog(DataTable logTable)
{
this.listBox1.DataSource = logTable;
}
以上代碼在獲取數據時,將不會帶來任何UI線程的阻塞。
總結:
寫下本文的主要目的在于總結以非阻塞模式調用函數的方法,我們應當了解以下結論;
● Delegate會對BeginInvoke與EndInvoke的調用生成正確的參數,所有的傳出參數、返回值與異常都可以在EndInvoke中取得。
● 不要忘記BeginInvoke是取自線程池中的線程,要注意防止異步任務的數量超過了線程池的線程上限值。
● CallBack委托表示對與異步任務的回調,它將使我們從阻塞的困擾中徹底解脫。
● 截止到目前為止,UI線程在處理異步工作時將不再阻塞,而只有在更新UI具體內容時才會發生阻塞。
問題
我們將發現,一旦數據量較大,我們的UI線程在裝載這些數據到控件的時候,依然會發生”假死”的情況。這是正常的,因為我們只保證了獲取數據與UI線程的獨立性,并沒有保證更新UI帶來的線程忙碌問題,”假死”正是UI線程忙碌帶來的一個用戶感受,如何避免這種情況,下文繼續介紹。
本站文章除注明轉載外,均為本站原創或翻譯。歡迎任何形式的轉載,但請務必注明出處、不得修改原文相關鏈接,如果存在內容上的異議請郵件反饋至chenjj@fc6vip.cn
文章轉載自:博客園