上一次提到了如何跨線程訪問GUI。而這個需求往往是異步操作導致的。今天我們就來看看Jeffrey Richter寫的AsyncEnumerator如何幫助我們處理異步問題。
先來看看最簡單的一段異步下載網頁的代碼:
public class Program
{
private static WebRequest request;
public static void Main(string[] args)
{
request = WebRequest.Create("http://www.thoughtworks.com.cn");
request.BeginGetResponse(HandleResponse, null);
Console.ReadLine();
}
private static void HandleResponse(IAsyncResult ar)
{
request.EndGetResponse(ar);
Console.WriteLine("Get response from web");
}
}
很簡單不是嗎?如果我們下載之后還要異步存儲到本地的磁盤,這個時候就不是那么容易了:
Code
代碼太長了,以至于我不得不折疊起來。這段代碼還是有問題的,因為它沒有處理異常情況,中途出個錯,文件就不會被關閉。
從邏輯上來說獲取Response,讀取Response流,寫入本地文件流從執行順序上來說是一個完成之后,然后過一會兒下個接上執行。理論上來講它就是
HandleGetResponse(xxx);
while(NotFinished(xxx)) {
HandleReadResponseStream(xxx);
HandleWriteFileStream(xxx);
}
CleanUp();
但是我們不能這么寫。因為在每個操作之間都是一個異步等待的過程。實際上,是因為異步操作把一個完成的流程打散到了多個回調函數中去完成。那么有什么辦法可以讓一個方法執行一段,然后等一會,再執行一段呢?有,這就是yield。yield代表我暫時放棄執行的權利,等IO完成之后你再來執行我,我接著干下面的操作。
private static void Demo()
{
int i = 1;
foreach (var fib in Fib())
{
Console.WriteLine(i + ": " + fib);
if (i++ > 10)
{
break;
}
}
}
private static IEnumerable<int> Fib()
{
int i = 1;
int j = 1;
yield return i;
yield return j;
while (true)
{
var k = i + j;
yield return k;
i = j;
j = k;
}
}
這個例子中,Fib(斐波那契額數列)是一個死循環。如果它是一個普通的函數,你是不能執行它的,因為它永遠不會放棄執行權,它會一只拽著CPU去算終極的fib。但是我們這個例子中的Fib不會。它在每次yield return的時候,都會跳出函數,返回到調用的地方。然后每次調用,都會從上次執行的地方繼續下去,繼續執行的時候所有的局部狀態(局部變量的值)都保留著上次的值。在foreach的背后是這么一個過程:
var enumerable = Fib();
var enumerator = enumerable.GetEnumerator();
enumerator.MoveNext(); //Fib被執行 return i;
Console.WriteLine(enumerator.Current);
enumerator.MoveNext(); //Fib被繼續執行 return j;
Console.WriteLine(enumerator.Current);
enumerator.MoveNext(); //Fib被繼續執行 return i+j;
Console.WriteLine(enumerator.Current);
所以我們只要把上面的IO操作序列,稍微改寫就可以讓它們不四處散落了:
BeginGetResponse(xxx);
yield return 1;
EndGetReponse(xxx);
while(NotFinished(xxx)) {
BeginReadResponseStream(xxx);
yield return 1;
EndGetResponseStream(xxx);
BeginWriteFileStream(xxx);
yield return 1;
EndGetResponseStream(xxx);
}
CleanUp();
因為每次yield return都會放棄執行權,所以我們可以在這個函數外的某處等待BeginXXX操作的回調,等IO操作完成了再來繼續執行這個函數。基于這個想法(最開始是這位仁兄想出來的http://msmvps.com/blogs/mihailik/archive/2005/12/26/79813.aspx),Jeffrey Richter寫了Power Threading庫(wintellect上的下載鏈接壞了,用這個http://www.wintellect.com/Downloads/PowerThreadingAttachments/Wintellect_Power_Threading_Library_(May_15,_2008).zip)。最后的代碼是這個樣子的:
private static IEnumerator<int> Download(AsyncEnumerator ae)
{
var webRequest = WebRequest.Create("http://www.thoughtworks.com.cn");
webRequest.BeginGetResponse(ae.End(), null);
yield return 1;
var response = webRequest.EndGetResponse(ae.DequeueAsyncResult());
Console.WriteLine("Get response from web");
var buffer = new byte[4096];
var count = buffer.Length;
using (var responseStream = response.GetResponseStream())
{
using (var fileStream = new FileStream(@"c:\downloaded.html", FileMode.Create))
{
while (count > 0)
{
Console.WriteLine("Read a chunk");
responseStream.BeginRead(buffer, 0, buffer.Length, ae.End(), null);
yield return 1;
count = responseStream.EndRead(ae.DequeueAsyncResult());
Console.WriteLine("Write a chunk");
fileStream.BeginWrite(buffer, 0, count, ae.End(), null);
yield return 1;
fileStream.EndWrite(ae.DequeueAsyncResult());
}
}
}
Console.WriteLine("Finished downloading from response");
}
是不是很簡單呢?不過還有一個問題,那就是yield return我明白,是為了暫時退出這個函數,等待異步操作完成之后繼續執行。但是我不明白的是,為什么是yield return 1呢?
其實這個yield return 1是給另外一個高級功能使用的。它的意思是“等待1個異步操作結束,然后執行我這行之后的代碼“。如果yield return 2,就是等待兩個異步操作。所以你必須先begin兩個異步操作,然后yield return 2去等待。AsyncEnumerator還有返回值等高級功能,并且AsycnEnumerator內部使用了上文提到的AsyncOperationManager,所以在你的代碼中可以安全地操作GUI不用害怕跨線程的問題。
參考資料:
Asynchronous iterators:
http://msmvps.com/blogs/mihailik/archive/2005/12/26/79813.aspx
Simplified APM With The AsyncEnumerator:
http://msdn.microsoft.com/en-us/magazine/cc546608.aspx
Simplified APM with C#:
http://msdn.microsoft.com/en-us/magazine/cc163323.aspx
More AsyncEnumerator Features:
http://msdn.microsoft.com/en-us/magazine/cc721613.aspx
Using C# 2.0 iterators to simplify writing asynchronous code:
http://blogs.msdn.com/michen/archive/2006/03/30/using-c-2-0-iterators-to-simplify-writing-asynchronous-code.aspx
http://blogs.msdn.com/michen/archive/2006/04/01/using-c-2-0-iterators-to-simplify-writing-asynchronous-code-part-2.aspx
附記:
喂,博主啊?為什么不直接創建一個新線程,然后在那里用同步操作完成上述動作?這個問題在我這里等價為什么要使用.NET的APM(Asynchronous Programming Model,異步編程模型)。正確的答案,參見http://msdn.microsoft.com/en-us/magazine/cc301191.aspx,Jeffrey Richter肯定寫過這個問題的答案。不那么正確的答案:
1、提供如何實現異步操作的靈活性,新線程只是很多實現中的一種
這樣我們可以利用Windows的Overlapped I/O,而這個就是一個內核級別的回調,不牽涉線程的問題了。性能直追epoll。
2、提供了何時使用新線程的靈活性,在一開始創建一個新線程然后把所有代碼放到那里同步執行只是其中一種。
一個很流行的idea,叫SEDA(Staged Event Driven Architecture),究其核心就是把長操作分解成為異步的短操作,然后用不同大小的Thread Pool來回調不同類型的異步操作,通過調優達到線程在stage之間的最佳配比。這樣避免了一有請求就起新線程的開銷,線程多了系統就響應不過來了。又避免了單線程異步回調的低資源利用率,特別是CPU已經多核了的情況下。利用APM和AsyncEnumerator,再加上自己實現的ThreadPool,做一個.NET版本的SEDA架構也是可能的。
浙公網安備 33010602011771號