C#(99):三、.NET 4.0基於任務的非同步模式(TAP),推薦使用
三、基於任務的非同步模式(TAP),推薦使用
一、引言
當使用APM的時候,首先我們要先定義用來包裝回調方法的委託,這樣難免有點繁瑣,然而使用EAP的時候,我們又需要實現Completed事件和Progress事件,上面兩種實現方式感覺都有點繁瑣。
同時微軟也意識到了這點,所以在.NET 4.0中提出了一個新的非同步模式——基於任務的非同步模式TAP(Task-based Asynchronous Pattern )。
基於任務的非同步模式 (TAP) 是基於
二、什麼是TAP——基於任務的非同步模式介紹
當看到類中存在TaskAsync為字尾的方法時就代表該類實現了TAP,並且基於任務的非同步模式同樣也支援非同步操作的取消和進度的報告的功能。
在TAP實現中,我們只需要通過向非同步方法傳入CancellationToken 引數,因為在非同步方法內部會對這個引數的IsCancellationRequested
在TAP中,我們可以通過IProgress<T>介面來實現進度報告的功能。
1、計算密集型任務
例如,請考慮使用呈現影象的非同步方法。
任務的主體可以輪詢取消標記,如果在呈現過程中收到取消請求,程式碼可提前退出。 此外,如果啟動之前收到取消請求,你需要阻止操作:
internal Task<Bitmap> RenderAsync(ImageData data,CancellationToken cancellationToken) { return Task.Run(() => {var bmp = new Bitmap(data.Width,data.Height); for(int y=0; y<data.Height; y++) { cancellationToken.ThrowIfCancellationRequested(); int x=0; x<data.Width; x++) { // render pixel [x,y] into bmp } } return bmp; },0);">cancellationToken); }
2、I/O 密集型任務:
假設你想建立一個將在指定時間段後完成的任務。 例如,你可能想延遲使用者介面中的活動。
System.Threading.Timer 類已提供在指定時間段後以非同步方式呼叫委託的能力,並且你可以通過使用 TaskCompletionSource<TResult> 將 Task<TResult> 前端放在計時器上,例如:
public static Task<DateTimeOffset> Delay(int millisecondsTimeout) { TaskCompletionSource<DateTimeOffset> tcs = null; Timer timer = ; timer = new Timer(delegate { timer.Dispose(); tcs.TrySetResult(DateTimeOffset.UtcNow); },,Timeout.Infinite,Timeout.Infinite); tcs = new TaskCompletionSource<DateTimeOffset>(timer); timer.Change(millisecondsTimeout,Timeout.Infinite); tcs.Task; }從 .NET Framework 4.5 開始,Task.Delay 方法正是為此而提供的,並且你可以在另一個非同步方法內使用它。例如,若要實現非同步輪詢迴圈:
static async Task Poll(Uri url,0);">CancellationToken cancellationToken,IProgress<bool> progress) { while(true) { await Task.Delay(TimeSpan.FromSeconds(10),cancellationToken); bool success = false; try { await DownloadStringAsync(url); success = ; } catch { /* ignore errors */ } progress.Report(success); } }三、如何使用TAP——使用基於任務的非同步模式來非同步程式設計
下面就讓我們實現自己的非同步方法(亮點為只需要一個方法就可以完成進度報告和非同步操作取消的功能)
Download File CancellationToken 引數賦值獲得一個取消請求 progress引數負責進度報告 private void DownLoadFile(string url,0);">CancellationToken ct,IProgress<int> progress) { HttpWebRequest request = ; HttpWebResponse response = ; Stream responseStream = ; int bufferSize = 2048byte[] bufferBytes = new byte[bufferSize]; { request = (HttpWebRequest)WebRequest.Create(url); if (DownloadSize != 0) { request.AddRange(DownloadSize); } response = (HttpWebResponse)request.GetResponse(); responseStream = response.GetResponseStream(); int readSize = while () { 收到取消請求則退出非同步操作 if (ct.IsCancellationRequested == ) { MessageBox.Show(String.Format("下載暫停,下載的檔案地址為:{0}\n 已經下載的位元組數為: {1}位元組" { this.btnStart.Enabled = ; this.btnPause.Enabled = ; },0);">); 退出非同步操作 break; } readSize = responseStream.Read(bufferBytes,if (readSize > ) { DownloadSize += readSize; int percentComplete = (int)((float)DownloadSize / (float)totalSize * 100); filestream.Write(bufferBytes, 報告進度 progress.Report(percentComplete); } else { MessageBox.Show(String.Format(下載已完成,下載的檔案地址為:{0},檔案的總位元組數為: {1}位元組); response.Close(); filestream.Close(); ; } } } catch (AggregateException ex) { 因為呼叫Cancel方法會丟擲OperationCanceledException異常 將任何OperationCanceledException物件都視為以處理 ex.Handle(e => e is OperationCanceledException); } }
這樣只需要上面的一個方法,我們就可以完成上一專題中檔案下載的程式,我們只需要在下載按鈕的事件處理程式呼叫該方法和在暫停按鈕的事件處理程式呼叫CancellationTokenSource.Cancel方法即可,具體程式碼為:
#region 欄位 int DownloadSize = ; string downloadPath = long totalSize = private FileStream filestream; private CancellationTokenSource cts = private Task task = null; SynchronizationContext sc; #endregion 欄位 #region 建構函式 public FileDownLoadForm() { InitializeComponent(); string url = http://download.microsoft.com/download/7/0/3/703455ee-a747-4cc8-bd3e-98a615c3aedb/dotNetFx35setup.exe; txbUrl.Text = url; ; Get Total Size of the download file GetTotalSize(); downloadPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + \\" + Path.GetFileName(this.txbUrl.Text.Trim()); if (File.Exists(downloadPath)) { FileInfo fileInfo = FileInfo(downloadPath); DownloadSize = ()fileInfo.Length; if (DownloadSize == totalSize) { string message = There is already a file with the same name,0);">do you want to delete it? If not,please change the local path. ; var result = MessageBox.Show(message,0);">File name conflict: " + downloadPath,MessageBoxButtons.OKCancel); if (result == System.Windows.Forms.DialogResult.OK) { File.Delete(downloadPath); } { progressBar1.Value = (); ; ; } } } } #endregion 建構函式 #region 方法 Start DownLoad File void btnStart_Click(object sender,EventArgs e) { filestream = FileStream(downloadPath,FileMode.OpenOrCreate); ; filestream.Seek(DownloadSize,SeekOrigin.Begin); 捕捉呼叫執行緒的同步上下文派生物件 sc = SynchronizationContext.Current;
cts = new CancellationTokenSource(); 使用指定的操作初始化新的 Task。 task = new Task(() => Actionmethod(cts.Token),0);">cts.Token); 啟動 Task,並將它安排到當前的 TaskScheduler 中執行。 task.Start(); await DownLoadFileAsync(txbUrl.Text.Trim(),cts.Token,new Progress<int>(p => progressBar1.Value = p)); } 任務中執行的方法 void Actionmethod(CancellationToken ct) { 使用同步上文文的Post方法把更新UI的方法讓主執行緒執行 DownLoadFile(txbUrl.Text.Trim(),0);">ct,255);">new Progress<int>(p => { sc.Post(new SendOrPostCallback((result) => progressBar1.Value = ()result),p); })); } Pause Download void btnPause_Click( 發出一個取消請求 cts.Cancel(); } Get Total Size of File GetTotalSize() { HttpWebRequest myHttpWebRequest = (HttpWebRequest)WebRequest.Create(txbUrl.Text.Trim()); HttpWebResponse response = (HttpWebResponse)myHttpWebRequest.GetResponse(); totalSize = response.ContentLength; response.Close(); }
四、與其他非同步模式和型別互操作
1、從 APM 到 TAP
可以使用 TaskFactory<TResult>.FromAsync 方法來實現此操作的 TAP 包裝,如下所示:
static Task<int> ReadAsync( Stream stream,255);">byte[] buffer, offset,0);"> count) { if (stream == ) throw new ArgumentNullException(stream); return Task<.Factory.FromAsync(stream.BeginRead,stream.EndRead,buffer,offset,count,0);">); }
此實現類似於以下內容:
byte [] buffer,0);"> count) { ) ); var tcs = new TaskCompletionSource<int>();
stream.BeginRead(buffer,iar => { { tcs.TrySetResult(stream.EndRead(iar)); } (OperationCanceledException) { tcs.TrySetCanceled(); } (Exception exc) { tcs.TrySetException(exc); } },0);">); tcs.Task; }
2、從EAP到 TAP
string> DownloadStringAsync(Uri url) { string> var wc = WebClient();
wc.DownloadStringCompleted += (s,e) => { if (e.Error != ) tcs.TrySetException(e.Error); else (e.Cancelled) tcs.TrySetCanceled(); tcs.TrySetResult(e.Result); }; wc.DownloadStringAsync(url); tcs.Task; }