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