當我們處理一些長線的調用時,經常會導致界面停止響應或者IIS線程占用過多等問題,這個時候我們需要更多的是用異步編程來修正這些問題,但是通常都是說起來容易做起來難,誠然異步編程相對於同步編程來說,它是一種完全不同的編程思想,對於習慣了同步編程的開發者來說,在開發過程中難度更大,可控性不強是它的特點。
在.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,每一個調用都創建了一個事件回調。代碼用遞歸取代了循環,這種代碼你應該都不敢直視了吧。不要著急,還沒有完 。
原始的代碼返回了一個總數並且顯示它,新的一步版本在統計還沒有完成之前返回給調用者。我們怎麼樣才可以得到一個結果返回給調用者,答案是:調用者必須支持一個回掉,我們可以在統計完成之後調用它。
然而異常怎麼辦?原始的代碼並沒有關注異常,它會一直傳遞給調用者,在異步版本中,我們必須擴展回掉來讓異常來傳播,在異常發生時,我們不得不明確的讓它傳播。
最終,這些需要將會進一步讓代碼混亂:
public void SumpageSizesAsync(IList uris,Actionint,Exception> callback)
{
SumPageSizesAsyncHelper(uris.GetEnumerator(), 0, callback);
}
public void SumPageSizesAsyncHelper(IEnumerator enumerator, int total,Actionint,Exception> callback)
{
try
{
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,callback);
};
client.DownloadDataAsync(enumerator.Current);
}
else
{
txtStatus.Text = string.Format("Found {0} bytes total", total);
enumerator.Dispose();
callback(total, null);
}
}
catch (Exception ex)
{
enumerator.Dispose();
callback(0, ex);
}
}
當你再看這些代碼的時候,你還能立馬清楚的說出這是什麼JB玩意嗎?
恐怕不能,我們開始只是想和同步方法那樣只是用一個異步的調用來替換阻塞的調用,讓它包裝在一個foreach循環中,想想一下試圖去組合更多的異步調用或者有更復雜的控制結構,這不是一個SubPageSizesAsync的規模能解決的。
我們的真正問題在於我們不再可以解釋這些方法裡的邏輯,我們的代碼已經完全無章可循。異步代碼中很多的工作讓整件事情看起來難以閱讀並且似乎充滿了BUG。
一個新的方式
如今,我們擁有了一個新的功能來解決上述的問題,異步版本的代碼將會如下文所示:
public async Taskint> SumPageSizesAsync(IList uris)
{
int total = 0;
foreach (var uri in uris)
{
txtStatus.Text = string.Format("Found {0} bytes...", total);
var data = await new WebClient().DownloadDataTaskAsync(uri);
total += data.Length;
}
txtStatus.Text = string.Format("Found {0} bytes total", total);
return total;
}
除了添加的高亮的部分,上文中的代碼與同步版本的代碼非常相似,代碼的流程也從未改變,我們也沒有看到任何的回調,但是這並不代表實際上沒有回調操作,編譯器會搞定這些工作,不再需要您去關心。
異步的方法是用了Task替代了原來返回的Int類型,Task和Task是在如今的framework提供的,用來代表一個正在運行的工作。
異步的方法沒有額外的方法,依照慣例為了區別同步版本的方法,我們在方法名後添加Async作為新的方法名。上文中的方法也是異步的,這表示方法體會讓編譯器區別對待,允許其中的一部分將會變成回調,並且自動的創建Task作為返回類型。
關於這個方法的解釋:在方法內部,調用另外一個異步方法DownloadDataTaskAsync,它快速的返回一個Task類型的變量,它會在下載數據完成以後被激活,到如前為止,在數據沒有完成之前,我們不想做任何事,所以我們使用await來等待操作的完成。
看起來await關鍵字阻塞了線程直到task完成下載的數據可用,其實不然,相反它標志了任務的回調,並且立即返回,當這個任務完成之後,它會執行回調。
Tasks
Task和Task類型已經存在於.NET Framework 4.0中,一個Task代表一個進行時的活動,它可能是一個運行在單獨線程中的一個CPU密集型的工作或者一個IO操作,手動的創建一個不工作在單