C#(99):三、.NET 4.0基於任務的異步模式(TAP),推薦使用




一、引言

當使用APM的時候,首先我們要先定義用來包裝回調方法的委托,這樣難免有點繁瑣, 然而使用EAP的時候,我們又需要實現Completed事件和Progress事件,上面兩種實現方式感覺都有點繁瑣。

同時微軟也意識到了這點,所以在.NET 4.0中提出了一個新的異步模式——基於任務的異步模式TAP(Task-based Asynchronous Pattern )。

基於任務的異步模式 (TAP) 是基於 System.Threading.Tasks.Task 命名空間中的 System.Threading.Tasks.Task System.Threading.Tasks 類型,這些類型用於表示任意異步操作。是用於新開發的建議的異步設計模式。

二、什么是TAP——基於任務的異步模式介紹

當看到類中存在TaskAsync為后綴的方法時就代表該類實現了TAP, 並且基於任務的異步模式同樣也支持異步操作的取消進度的報告的功能。

在TAP實現中,我們只需要通過向異步方法傳入CancellationToken 參數,因為在異步方法內部會對這個參數的IsCancellationRequested屬性進行監控,當異步方法收到一個取消請求時,異步方法將會退出執行。

在TAP中,我們可以通過IProgress 接口來實現進度報告的功能。

參考:二、並行編程 - Task任務

1、計算密集型任務

例如,請考慮使用呈現圖像的異步方法。

任務的主體可以輪詢取消標記,如果在呈現過程中收到取消請求,代碼可提前退出。 此外,如果啟動之前收到取消請求,你需要阻止操作:

internal Task
  
  
  
          
   
   
   
            RenderAsync(ImageData data, CancellationToken cancellationToken) { 
   
   
   
           return Task.Run(() =>
   
   
   
            { 
   
   
   
           var bmp = 
   
   
   
           new
   
   
   
            Bitmap(data.Width, data.Height); 
   
   
   
           for(
   
   
   
           int y=
   
   
   
           0; y
   
   
   
           
             ) { 
            cancellationToken.ThrowIfCancellationRequested(); 
            for( 
            int x= 
            0; x 
            
              ) { 
             // 
              render pixel [x,y] into bmp 
              } }  
             return 
              bmp; }, cancellationToken); } 
             
           
  
  
  
          

2、I/O 密集型任務:

假設你想創建一個將在指定時間段后完成的任務。 例如,你可能想延遲用戶界面中的活動。

System.Threading.Timer 類已提供在指定時間段后以異步方式調用委托的能力,並且你可以通過使用 TaskCompletionSource Task 前端放在計時器上,例如:

public static Task
  
  
  
          
           
   
   
    Delay(
   
   
   
           int
   
   
   
            millisecondsTimeout) { TaskCompletionSource
   
   
   
            
             tcs = 
   
   
   
           null
   
   
   
           ; Timer timer = 
   
   
   
           null
   
   
   
           ; timer = 
   
   
   
           new Timer(
   
   
   
           delegate
   
   
   
            { timer.Dispose(); tcs.TrySetResult(DateTimeOffset.UtcNow); }, 
   
   
   
           null
   
   
   
           , Timeout.Infinite, Timeout.Infinite); tcs =
   
   
   
            new TaskCompletionSource 
            
   
   
   
           (timer); timer.Change(millisecondsTimeout, Timeout.Infinite); 
   
   
   
           return
   
   
   
            tcs.Task; }
  
  
  
          

從 .NET Framework 4.5 開始,Task.Delay 方法正是為此而提供的,並且你可以在另一個異步方法內使用它。例如,若要實現異步輪詢循環:

public static async Task Poll(Uri url, CancellationToken cancellationToken,  IProgress<bool> progress)
{
    while(true)
    {
        await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
        bool success = false;
        try
        {
            await DownloadStringAsync(url);
            success = true;
        }
        catch { /* ignore errors */ }
        progress.Report(success);
    }
}

三、如何使用TAP——使用基於任務的異步模式來異步編程

下面就讓我們實現自己的異步方法(亮點為只需要一個方法就可以完成進度報告和異步操作取消的功能)

// Download File CancellationToken 參數賦值獲得一個取消請求 progress參數負責進度報告
private void DownLoadFile(string url, CancellationToken ct, IProgress<int> progress)
{
    HttpWebRequest request = null;
    HttpWebResponse response = null;
    Stream responseStream = null;
    int bufferSize = 2048;
    byte[] bufferBytes = new byte[bufferSize];
    try
    {
        request = (HttpWebRequest)WebRequest.Create(url);
        if (DownloadSize != 0)
        {
            request.AddRange(DownloadSize);
        }

        response = (HttpWebResponse)request.GetResponse();
        responseStream = response.GetResponseStream();
        int readSize = 0;
        while (true)
        {
            // 收到取消請求則退出異步操作
            if (ct.IsCancellationRequested == true)
            {
                MessageBox.Show(String.Format("下載暫停,下載的文件地址為:{0}\n 已經下載的字節數為: {1}字節", downloadPath, DownloadSize));

                response.Close();
                filestream.Close();
                sc.Post((state) =>
                {
                    this.btnStart.Enabled = true;
                    this.btnPause.Enabled = false;
                }, null);

                // 退出異步操作
                break;
            }

            readSize = responseStream.Read(bufferBytes, 0, bufferBytes.Length);
            if (readSize > 0)
            {
                DownloadSize += readSize;
                int percentComplete = (int)((float)DownloadSize / (float)totalSize * 100);
                filestream.Write(bufferBytes, 0, readSize);

                // 報告進度
                progress.Report(percentComplete);
            }
            else
            {
                MessageBox.Show(String.Format("下載已完成,下載的文件地址為:{0},文件的總字節數為: {1}字節", downloadPath, totalSize));

                sc.Post((state) =>
                {
                    this.btnStart.Enabled = false;
                    this.btnPause.Enabled = false;
                }, null);

                response.Close();
                filestream.Close();
                break;
            }
        }
    }
    catch (AggregateException ex)
    {
        // 因為調用Cancel方法會拋出OperationCanceledException異常 將任何OperationCanceledException對象都視為以處理
        ex.Handle(e => e is OperationCanceledException);
    }
}

這樣只需要上面的一個方法,我們就可以完成上一專題中文件下載的程序,我們只需要在下載按鈕的事件處理程序調用該方法和在暫停按鈕的事件處理程序調用CancellationTokenSource.Cancel方法即可,具體代碼為:

#region 字段

private int DownloadSize = 0;
private string downloadPath = null;
private long totalSize = 0;
private FileStream filestream;

private CancellationTokenSource cts = null; private Task task = null;

private 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;
    this.btnPause.Enabled = false;

    // 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 = new FileInfo(downloadPath);
        DownloadSize = (int)fileInfo.Length;
        if (DownloadSize == totalSize)
        {
            string message = "There is already a file with the same name, do you want to delete it? If not, please change the local path. ";
            var result = MessageBox.Show(message, "File name conflict: " + downloadPath, MessageBoxButtons.OKCancel);

            if (result == System.Windows.Forms.DialogResult.OK)
            {
                File.Delete(downloadPath);
            }
            else
            {
                progressBar1.Value = (int)((float)DownloadSize / (float)totalSize * 100);
                this.btnStart.Enabled = false;
                this.btnPause.Enabled = false;
            }
        }
    }
}

#endregion 構造函數

#region 方法

// Start DownLoad File
private void btnStart_Click(object sender, EventArgs e)
{
    filestream = new FileStream(downloadPath, FileMode.OpenOrCreate);
    this.btnStart.Enabled = false;
    this.btnPause.Enabled = true;

    filestream.Seek(DownloadSize, SeekOrigin.Begin);

    // 捕捉調用線程的同步上下文派生對象
    sc = SynchronizationContext.Current;
 cts = new CancellationTokenSource();
    // 使用指定的操作初始化新的 Task。
   task = new Task(() => Actionmethod(cts.Token), cts.Token);

    // 啟動 Task,並將它安排到當前的 TaskScheduler 中執行。
    task.Start(); 
    //await DownLoadFileAsync(txbUrl.Text.Trim(), cts.Token,new Progress
   
   
   
           
             (p => progressBar1.Value = p)); 
           
}

// 任務中執行的方法
private void Actionmethod(CancellationToken ct)
{
    // 使用同步上文文的Post方法把更新UI的方法讓主線程執行
    DownLoadFile(txbUrl.Text.Trim(), ct, new Progress<int>(p =>
        {
            sc.Post(new SendOrPostCallback((result) => progressBar1.Value = (int)result), p);
        }));
}

// Pause Download
private void btnPause_Click(object sender, EventArgs e)
{
    // 發出一個取消請求
    cts.Cancel();
}

// Get Total Size of File
private void GetTotalSize()
{
    HttpWebRequest myHttpWebRequest = (HttpWebRequest)WebRequest.Create(txbUrl.Text.Trim());
    HttpWebResponse response = (HttpWebResponse)myHttpWebRequest.GetResponse();
    totalSize = response.ContentLength;
    response.Close();
}

四、與其他異步模式和類型互操作

1、從 APM 到 TAP

可以使用 TaskFactory .FromAsync 方法來實現此操作的 TAP 包裝,如下所示:

public static Task<int> ReadAsync(this Stream stream,  byte[] buffer, int offset,  int count)
{
    if (stream == null) 
       throw new ArgumentNullException("stream");
    
    return Task<int>.Factory.FromAsync(stream.BeginRead, stream.EndRead, buffer,   offset, count, null);
}

此實現類似於以下內容:

 public static Task<int> ReadAsync(this Stream stream,  byte [] buffer, int offset,   int count)
 {
    if (stream == null) 
        throw new ArgumentNullException("stream");

    var tcs = new TaskCompletionSource<int>();

    stream.BeginRead(buffer, offset, count, iar =>
                     {
                        try { 
                           tcs.TrySetResult(stream.EndRead(iar)); 
                        }
                        catch(OperationCanceledException) { 
                           tcs.TrySetCanceled(); 
                        }
                        catch(Exception exc) { 
                           tcs.TrySetException(exc); 
                        }
                     }, null);
    return tcs.Task;
}

2、從EAP到 TAP

public static Task<string> DownloadStringAsync(Uri url)
 {
     var tcs = new TaskCompletionSource<string>();

     var wc = new WebClient();

     wc.DownloadStringCompleted += (s,e) =>
         {
             if (e.Error != null) 
                tcs.TrySetException(e.Error);
             else if (e.Cancelled) 
                tcs.TrySetCanceled();
             else 
                tcs.TrySetResult(e.Result);
         };
     wc.DownloadStringAsync(url);
     return tcs.Task;
}



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM