在.NET
Framework5.0種,微軟為我們系統了新的語言特性,讓我們使用異步編程就像使用同步編程壹樣相近和簡單,本文中將會解釋以前版本的Framework中基於回調道德異步編程模型的壹些限制以及新型的API如果讓我們簡單的做到同樣的開發任務。
為什麽要異步
壹直以來,使用遠程資源的編程都是壹個容易造成困惑的問題,不同於?本地資源?,遠程資源的訪問總會有很多意外的情況,網絡環境的不穩定機器服務端的故障,會造成很多程序員完全不可控的問題,所以這也就要求程序員需要更多的去保護遠程資源的調用,管理調用的取消、超市、線程的等待以及處理線程長時間沒響應的情況等。而在.NET中我們通常忽略了這些挑戰,事實上我們會有多種不用的模式來處理異步編程,比如在處理IO密集型操作或者高延遲的操作時候不組測線程,多數情況我們擁有同步和異步兩個方法來做這件事。可是問題在於當前的這些模式非常容易引起混亂和代碼錯誤,或者開發人員會放棄然後使用阻塞的方式去開發。
而在如今的.NET中,提供了非常接近於同步編程的編程體驗,不需要開發人員再去處理只會在異步編程中出現的很多情況,異步調用將會是清晰的且不透明的,而且易於和同步的代碼進行組合使用。
過去糟糕的體驗
最好的理解這種問題的方式是我們最常見的壹種情況:用戶界面只擁有壹個線程所有的工作都運行在這個線程上,客戶端程序不能對用戶的鼠標時間做出反應,這很可能是因為應用程序正在被壹個耗時的操作所阻塞,這可能是因為線程在等待壹個網絡ID或者在做壹個CPU密集型的計算,此時用戶界面不能獲得運行時間,程序壹直處於繁忙的狀態,這是壹個非常差的用戶體驗。
很多年來,解決這種問題的方法都是做異步花的調用,不要等待響應,盡快的返回請求,讓其他事件可以同時執行,只是當請求有了最終反饋的時候通知應用程序讓客戶代碼可以執行指定的代碼。
而問題在於:異步代碼完全毀掉了代碼流程,回調代理解釋了之後如何工作,但是怎麽在壹個while循環裏等待?壹個if語句?壹個try塊或者壹個using塊?怎麽去解釋?接下來做什麽?
看下面的壹個例子:
public
int
SumPageSizes(IList
uris)
{
int
total
=
0;
foreach
(var
uri
in
uris)
{
txtStatus.Text
=
string.Format("Found
{0}
bytes...",
total);
var
data
=
new
WebClient().DownloadData(uri);
total
+=
data.Length;
}
txtStatus.Text
=
string.Format("Found
{0}
bytes
total",
total);
return
total;
}
這個方法從壹個uri列表裏下載文件,統計他們的大小並且同時更新狀態信息,很明顯這個方法不屬於UI線程因為它需要花費非常長的時間來完成,這樣它會完全的掛起UI,但是我們又希望UI能被持續的更新,怎麽做呢?
我們可以創建壹個後臺編程,讓它持續的給UI線程發送數據來讓UI來更新自身,這個看起來是很浪費的,因為這個線程把大多時間花在等下和下載上,但是有的時候,這正是我們需要做的。在這個例子中,WebClient提供了壹個異步版本的DownloadData方法?DownloadDataAsync,它會立即返回,然後在DownloadDataCompleted後觸發壹個事件,這允許用戶寫壹個異步版本的方法分割所要做的事,調用立即返回並完成接下來的UI線程上的調用,從而不再阻塞UI線程。下面是第壹次嘗試:
public
void
SumpageSizesAsync(IList
uris)
{
SumPageSizesAsyncHelper(uris.GetEnumerator(),
0);
}
public
void
SumPageSizesAsyncHelper(IEnumerator
enumerator,
int
total)
{
if
(enumerator.MoveNext())
{
txtStatus.Text
=
string.Format("Found
{0}
bytes...",
total);
var
client
=
new
WebClient();
client.DownloadDataCompleted
+=
(sender,e)=>{
SumPageSizesAsyncHelper(enumerator,
total
+
e.Result.Length);
};
client.DownloadDataAsync(enumerator.Current);
}
else
{
txtStatus.Text
=
string.Format("Found
{0}
bytes
total",
total);
}
}
然後這依然是糟糕的,我們破壞了壹個整潔的foreach循環並且手動獲得了壹個enumerator,每壹個調用都創建了壹個事件回調。代碼用遞歸取代了循環,這種代碼妳應該都不敢直視了吧。不要著急,還沒有完
原始的代碼返回了壹個總數並且顯示它,新的壹步版本在統計還沒有完成之前返回給調用者。我們怎麽樣才可以得到壹個結果返回給調用者,答案是:調用者必須支持壹個回掉,我們可以在統計完成之後調用它。
然而異常怎麽辦?原始的代碼並沒有關註異常,它會壹直傳遞給調用者,在異步版本中,我們必須擴展回掉來讓異常來傳播,在異常發生時,我們不得不明確的讓它傳播。