[C#.NET][TPL] 利用 Task.Factory.FromAsync / TaskCompletionSource 將 APM 轉換成 TAP

[C#.NET][TPL] 利用 Task.Factory.FromAsync / TaskCompletionSource 將 APM 轉換成 TAP

假使,類別裡面已經開發了 APM(Asynchronous Programming Model) 非同步模型,也就是 BegingXXX / EndXXX 方法,如果你已經受不了 APM 的所帶來的壞味道,我們可以利用 TPL 組合 BegingXXX / EndXXX 方法,另起爐灶..

內文連結

原型:嗅出壞味道

使用 Task.Factory.FromAsync 改善 APM 非同步模型

使用 Task.Factory.FromAsync + Task.ContinueWith 改善 APM 非同步模型

使用 TaskCompletionSource 改善 APM 非同步模型

await 到底回傳什麼?

APM 詳細建立方式可參考以下

http://www.dotblogs.com.tw/yc421206/archive/2012/10/25/78910.aspx

http://www.dotblogs.com.tw/yc421206/archive/2011/01/03/20540.aspx

有關 TAP 非同步建立步驟可參考以下

http://www.dotblogs.com.tw/yc421206/archive/2013/08/05/113433.aspx

 


原型:嗅出壞味道

ReadUrl 方法裡調用了BeginGetResponse / EndGetResponse,又利用 SynchronizationContext 處理跨執行緒 UI 更新,這段程式碼看起又臭又長,維護起來相當痛苦;APM 即使是相當方便的調用,但一旦無法跟UI邏輯切開難免還是會寫成一大包
 


private void ReadUrl(string url)
{
    WebRequest webRequest = WebRequest.Create(url);
    WebResponse webResponse = null;
    Stream stream = null;

    webRequest.BeginGetResponse(ar =>
    {
        try
        {
            webResponse = webRequest.EndGetResponse(ar);
            if (webResponse == null)
                return;

            stream = webResponse.GetResponseStream();
            if (stream == null)
                return;

            var content = "";
            if (webResponse.ContentType.ToLower().StartsWith("text/"))
            {
                var reader = new StreamReader(stream, System.Text.Encoding.UTF8);
                content = reader.ReadToEnd();

                this._synchronizationContext.Post(_ =>
                {
                    label_Length.Text = "Content length: " + webResponse.ContentLength.ToString();
                    label_Content.Text = content;
                }, null);
            }
        }
        catch (WebException we)
        {
            this._synchronizationContext.Post(_ =>
            {
                label_Length.Text = "Failed: " + we.GetBaseException().Message;
            }, null);
        }
        finally
        {
            if (stream != null)
            {
                stream.Close();
            }
            if (webResponse != null)
            {
                webResponse.Close();
            }

            if (webResponse == null || stream == null)
            {
                this._synchronizationContext.Post(_ =>
                {
                    label_Length.Text = "Content length: null";
                    label_Content.Text = "Empty";
                }, null);
            }
        }
    }, null);
}

調用 ReadUrl 方法


private void button_ReadURL_Click(object sender, EventArgs e)
{
    this.ReadUrl("http://wintellect.com/");
}

同樣的再看另外一個例子,方法體裡面有 UI 業務邏輯,能切開嗎?

 


private void ReadFile(string fileName)
{
    var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read);
    byte[] bufferArray = new byte[stream.Length];
    
    stream.BeginRead(bufferArray, 0, bufferArray.Length, ar =>
    {
        try
        {
            var result = stream.EndRead(ar);
            this._synchronizationContext.Post(_ =>
            {
                label_Length.Text = "Content length: " + result;
                label_Content.Text = Encoding.UTF8.GetString(bufferArray);
            }, null);
        }
        finally
        {
            if (stream != null)
                stream.Dispose();
        }
    }, null);
}

調用方法

private void button_ReadFile_Click(object sender, EventArgs e)
{
    this.ReadFile("TextFile1.txt");
}

 

APM 很好,但要怎麼做才能更好?


改善步驟如下:

  1. 命名規則以 Async 為後綴
  2. 返回 Task 或是 Task<TResult>
  3. 調用 TaskFactory.FromAsync  或 TaskCompletionSource  方法

 

使用 Task.Factory.FromAsync 改善 APM 非同步模型:

上面的例子實在是太糟了,可以利用 Task.Factory.FromAsync 來將 APM 轉換成建立 TAP 非同步模型:

private Task<WebResponse> ReadUrlAsync(string url)
{
    WebRequest webRequest = WebRequest.Create(url);
    //return Task.Run(() => Task<WebResponse>.Factory.FromAsync(webRequest.BeginGetResponse, webRequest.EndGetResponse, null));
    return Task<WebResponse>.Factory.FromAsync(webRequest.BeginGetResponse, webRequest.EndGetResponse, null);
}

 

使用 Task.Factory.FromAsync + Task.ContinueWith 改善 APM 非同步模型:

EndRead 預設是回傳 int ,在此動用 Task.ContinueWith 技巧讓方法體回傳 byte[],主要是強調技巧,在命名規則回傳型別還是不要差太多,不然會令人不知所措。


private Task<byte[]> ReadFileAsync(string fileName)
{
    Stream stream = null;
    stream = new FileStream(fileName, FileMode.Open, FileAccess.Read);
    var buffer = new byte[stream.Length];

    var mainTask = Task<int>.Factory.FromAsync(stream.BeginRead, stream.EndRead, buffer, 0, buffer.Length, null);
    var subTask = mainTask.ContinueWith(task =>
    {
        var length = task.Result;
        return length > 0 ? buffer : null;
    });
    return subTask;
}


另一個例子:


private Task<WebResponse> ReadUrlAsync1(string url)
{
    WebRequest webRequest = WebRequest.Create(url);
    var maskTask = Task.Factory.FromAsync<WebResponse>(webRequest.BeginGetResponse,
        webRequest.EndGetResponse, 
        null,
        TaskCreationOptions.None);
    var subTask = maskTask.ContinueWith(task =>
    {
        WebResponse webResponse = null;
        try
        {
            webResponse = task.Result;
            return webResponse;
        }
        catch (AggregateException ae)
        {
            throw;
        }
    });
    return subTask;
}


使用 TaskCompletionSource 改善 APM 非同步模型:

調用 TaskCompletionSource.SetResult 將結果取出。

改變 Task 狀態可調用以下三個方法

SetCanceledSetExceptionSetResult 


private Task<byte[]> ReadFileAsync1(string fileName)
{
    Stream stream = null;
    stream = new FileStream(fileName, FileMode.Open, FileAccess.Read);
    var buffer = new byte[stream.Length];
    var tcs = new TaskCompletionSource<byte[]>();
    stream.BeginRead(buffer, 0, buffer.Length, ar =>
    {
        try
        {
            var length = stream.EndRead(ar);
            tcs.SetResult(length > 0 ? buffer : null);
        }
        catch (Exception exc)
        {
            tcs.SetException(exc);
        }
    }, null);
    return tcs.Task;
}

await 到底回傳什麼?

調用 ReadFileAsync 方法,使用 async / await 關鍵字, ReadFileAsync 加上 await ,會回傳 byte[]


private async void button_ReadFileAsync_Click(object sender, EventArgs e)
{
    var result = await this.ReadFileAsync("TextFile1.txt");
    this.label_Length.Text = "Content length: " + result.Length;
    this.label_Content.Text = Encoding.UTF8.GetString(result);
}

調用 ReadUrlAsync 方法,使用 async / await 關鍵字,ReadUrlAsync 沒有加上 await,回傳 Task

private async void button_ReadUrlAsync_Click(object sender, EventArgs e)
{
    var task = ReadUrlAsync("http://wintellect.com/");
    await task;
    var webResponse = task.Result;
    if (webResponse.ContentType.ToLower().StartsWith("text/"))
    {
        var reader = new StreamReader(webResponse.GetResponseStream(), System.Text.Encoding.UTF8);
        this._synchronizationContext.Post(_ =>
        {
            label_Length.Text = "Content length: " + webResponse.ContentLength.ToString();
            label_Content.Text = reader.ReadToEnd();
        }, null);
    }
}

 


範例下載:https://dotblogsfile.blob.core.windows.net/user/yc421206/1308/201386124423465.zip

以上文章出自:http://www.dotblogs.com.tw/yc421206/archive/2013/08/06/113555.aspx

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo