public int SumPageSizes(IList<Uri> 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<Uri> uris) { SumPageSizesAsyncHelper(uris.GetEnumerator(), 0); } public void SumPageSizesAsyncHelper(IEnumerator<Uri> 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<Uri> uris,Action<int,Exception> callback) { SumPageSizesAsyncHelper(uris.GetEnumerator(), 0, callback); } public void SumPageSizesAsyncHelper(IEnumerator<Uri> enumerator, int total,Action<int,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 Task<int> SumPageSizesAsync(IList<Uri> 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>替代了原來返回的Int類型,Task和Task<T>是在如今的framework提供的,用來代表一個正在運行的工作。 異步的方法沒有額外的方法,依照慣例為了區別同步版本的方法,我們在方法名後添加Async作為新的方法名。上文中的方法也是異步的,這表示方法體會讓編譯器區別對待,允許其中的一部分將會變成回調,並且自動的創建Task<int>作為返回類型。 關於這個方法的解釋:在方法內部,調用另外一個異步方法DownloadDataTaskAsync,它快速的返回一個Task<byte[]>類型的變量,它會在下載數據完成以後被激活,到如前為止,在數據沒有完成之前,我們不想做任何事,所以我們使用await來等待操作的完成。 看起來await關鍵字阻塞了線程直到task完成下載的數據可用,其實不然,相反它標志了任務的回調,並且立即返回,當這個任務完成之後,它會執行回調。 Tasks Task和Task<T>類型已經存在於.NET Framework 4.0中,一個Task代表一個進行時的活動,它可能是一個運行在單獨線程中的一個CPU密集型的工作或者一個IO操作,手動的創建一個不工作在單獨線程的任務也是非常容易的:
static async void ReadAssignedFile() { byte[] buffer; try { double length = await ReadFileAsync("SomeFileDoNotExisted.txt", out buffer); } catch (Exception ex) { Console.WriteLine(ex.Message); } } static Task<double> ReadFileAsync(string filePath,out byte[] buffer) { Stream stream = File.Open(filePath, FileMode.Open); buffer = new byte[stream.Length]; var tcs = new TaskCompletionSource<double>(); stream.BeginRead(buffer, 0, buffer.Length, arr => { try { var length = stream.EndRead(arr); tcs.SetResult(stream.Length); } catch (IOException ex) { tcs.SetException(ex); } }, null); return tcs.Task; }
一旦創建了一個TaskCompletionSource對象,你就可以返回與它關聯的Task對象,問相關的工作完成後,客戶代碼才得到最終的結果,這時Task沒有占據自己的線程。 如果實際任務失敗,Task從樣可以攜帶異常並且向上傳播,如果使用await將觸發客戶端代碼的異常:
static async void ReadAssignedFile() { byte[] buffer; try { double length = await ReadFileAsync("SomeFileDoNotExisted.txt", out buffer); } catch (Exception ex) { Console.WriteLine(ex.Message); } } static Task<double> ReadFileAsync(string filePath,out byte[] buffer) { Stream stream = File.Open(filePath, FileMode.Open); buffer = new byte[stream.Length]; var tcs = new TaskCompletionSource<double>(); stream.BeginRead(buffer, 0, buffer.Length, arr => { try { var length = stream.EndRead(arr); tcs.SetResult(stream.Length); } catch (IOException ex) { tcs.SetException(ex); } }, null); return tcs.Task; }
image 基於任務的異步編程模型 上文中解釋了異步方法應該是的樣子-Task-based asynchronous Pattern(TAP),上文中異步的體現只需要一個調用方法和異步異步方法,後者返回一個Task或者Task<T>。 下文中將介紹一些TAP中的約定,包括怎麼處理“取消”和“進行中”,我們將進一步講解基於任務的編程模型。 Async和await 理解async方法不運行在自己的線程是非常重要的,事實上,編寫一個async方法但是沒有任何await的話,它就將會是一個不折不扣的同步方法:
static async Task<int> TenToSevenAsync() { Thread.Sleep(10000); return 7; }
假如你調用這個方法,將會阻塞線程10秒後返回7,這也許不是你期望的,在VS中也將得到一個警告,因為這可能永遠不是想要的結果。 只有一個async方法運行到一個await語句時,它才立即把控制權返回給調用方,然而只有當等待的任務完成之後,它才會真正的返回結果,這意味著你需要確保async方法中的代碼不會做過多的任務或者阻塞性能的調用。下面的實例才是你所期望的效果
static async Task<int> TenToSevenAsync() { await Task.Delay(3000); return 7; }
Task.Delay實際上是異步版本的Tread,Sleep,它返回一個Task,這個Task將會在指定的時間內完成。 時間處理程序和無返回值的異步方法 異步方法可以從其他異步方法使用await創建,但是異步在哪裡結束? 在客戶端程序中,通常的回答是異步方法由事件發起,用戶點擊一個按鈕,一個異步方法被激活,直到它完成,事件本身並不關系方法何時執行完成。這就是通常所說的“發後既忘” 為了適應這種模式,異步方法通常明確的被設計為“發後既忘”-使用void作為返回值替代Task<TResult>類型,這就讓方法可以直接作為一個事件處理程序。當一個void saync的方法執行時,沒有Task被返回,調用者也無法追蹤調用是否完成。
private async void someButton_Click(object sender, RoutedEventArgs e) { someButton.IsEnabled = false; await SumPageSizesAsync(GetUrls())); someButton.IsEnabled = true; }