2012年12月1日 星期六

用過會上癮的 async 與 await

在 C# 5.0 加入了 async 與 await 兩個 Keyword,基本用途是拿來修飾非同步的 Method,只要加上 async 的 Method 包括 Lambda 皆會被當作非同步方法來執行,想起來好像很簡單,用起來還真的也很簡單。

介紹的部份請至 MSDN 瀏覽:Asynchronous Programming with Async and Await
這裡簡單的寫些範例說明傳統的 delegate 與使用 async 寫法的差異,及使用 async 時的注意事項。

第一個範例,按下 Button 後使用傳統的 ThreadPool 執行一個長達 3 秒的背景作業,該作業內會每隔 1.5 秒印出一些文字及時間。可以從程式碼發現我們總共要寫兩個接受委派的 Method 分別名為 ActionWork 與 ActionCompleted。並且可以注意到 ActionWork 因為不在 UI Thread 執行,所以內部任何要在 lbl 這個 TextBlock 上輸出資訊的指令都必須使用 Dispatcher.RunAsync 包起來以確保不會發生 ThreadException,這個範例執行完成的輸出資訊看起來很直觀,Button Click Start 與 Button Click End 在 Background Thread 執行之前就先被執行輸出了。

private void OnButtonClick(object sender, RoutedEventArgs e)
{
    lbl.Text = "Button Click Start\n" + DateTime.Now.ToString("yyyy/MM/dd - hh:mm:ss") + "\n\n";
    IAsyncAction action = ThreadPool.RunAsync(ActionWork, WorkItemPriority.Normal);
    action.Completed = ActionCompleted;
    lbl.Text = lbl.Text + "Button Click End\n" + DateTime.Now.ToString("yyyy/MM/dd - hh:mm:ss") + "\n\n";
}

private void ActionWork(IAsyncAction sender)
{
    Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
    {
        lbl.Text = lbl.Text + "Delay Start\n" + DateTime.Now.ToString("yyyy/MM/dd - hh:mm:ss") + "\n\n";
    });
    DateTime dt = DateTime.Now;
    while(true)
    {
        if ((DateTime.Now - dt) > TimeSpan.FromSeconds(1.5))
        {
            break;
        }
    }
    Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
    {
        lbl.Text = lbl.Text + "Delaying\n" + DateTime.Now.ToString("yyyy/MM/dd - hh:mm:ss") + "\n\n";
    });
    dt = DateTime.Now;
    while (true)
    {
        if ((DateTime.Now - dt) > TimeSpan.FromSeconds(1.5))
        {
            break;
        }
    }
}

private void ActionCompleted(IAsyncAction source, AsyncStatus status)
{
    Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
    {
        lbl.Text = lbl.Text + "Delay End\n" + DateTime.Now.ToString("yyyy/MM/dd - hh:mm:ss") + "\n\n";
    });
}

// 輸出結果
Button Click Start
2012/11/30 - 23:28:17
     
Button Click End
2012/11/30 - 23:28:17

Delay Start
2012/11/30 - 23:28:17

Delaying
2012/11/30 - 23:28:18

Delay End
2012/11/30 - 23:28:20

第二個範例,我們改用 async 修飾 Delay 這個 Method,並且在按鍵事件中直接調用 Delay,可以看到輸出結果好像沒什麼差異,但至少我們可以知道 Delay 的確是在 Background Thread 執行的,而且在 Delay 裡面我們可以很方便的直接更新 UI 上的資訊。

private void OnButtonClick(object sender, RoutedEventArgs e)
{
    lbl.Text = "Button Click Start\n" + DateTime.Now.ToString("yyyy/MM/dd - hh:mm:ss") + "\n\n";
    Delay();
    lbl.Text = lbl.Text + "Button Click End\n" + DateTime.Now.ToString("yyyy/MM/dd - hh:mm:ss") + "\n\n";
}

private async void Delay()
{
    lbl.Text = lbl.Text + "Delay Start\n" + DateTime.Now.ToString("yyyy/MM/dd - hh:mm:ss") + "\n\n";
    await Task.Delay(1500);
    lbl.Text = lbl.Text + "Delaying\n" + DateTime.Now.ToString("yyyy/MM/dd - hh:mm:ss") + "\n\n";
    await Task.Delay(1500);
    lbl.Text = lbl.Text + "Delay End\n" + DateTime.Now.ToString("yyyy/MM/dd - hh:mm:ss") + "\n\n";
}

// 輸出結果
Button Click Start
2012/11/30 - 23:28:25

Delay Start
2012/11/30 - 23:28:25

Button Click End
2012/11/30 - 23:28:25

Delaying
2012/11/30 - 23:28:26

Delay End
2012/11/30 - 23:28:28

第三個範例我們稍微改了一下 Delay Method 宣告的傳回值,由 void 改為 Task 代表 Delay 由沒有傳回值變成傳回一個任務,執行結果和第二個範例沒什麼差異,但修改回傳值是一件非常重要的事,如果這個非同步的方法是可以被等待的,回傳值就必須為 Task 或 Task<T>。

private void OnButtonClick(object sender, RoutedEventArgs e)
{
    lbl.Text = "Button Click Start\n" + DateTime.Now.ToString("yyyy/MM/dd - hh:mm:ss") + "\n\n";
    Delay();
    lbl.Text = lbl.Text + "Button Click End\n" + DateTime.Now.ToString("yyyy/MM/dd - hh:mm:ss") + "\n\n";
}

private async Task Delay()
{
    lbl.Text = lbl.Text + "Delay Start\n" + DateTime.Now.ToString("yyyy/MM/dd - hh:mm:ss") + "\n\n";
    await Task.Delay(1500);
    lbl.Text = lbl.Text + "Delaying\n" + DateTime.Now.ToString("yyyy/MM/dd - hh:mm:ss") + "\n\n";
    await Task.Delay(1500);
    lbl.Text = lbl.Text + "Delay End\n" + DateTime.Now.ToString("yyyy/MM/dd - hh:mm:ss") + "\n\n";
}

// 輸出結果
Button Click Start
2012/11/30 - 23:28:30

Delay Start
2012/11/30 - 23:28:30

Button Click End
2012/11/30 - 23:28:30

Delaying
2012/11/30 - 23:28:31

Delay End
2012/11/30 - 23:28:33

第四個範例基本上和第三個範例很像,只是在按鍵事件裡調用 Delay 時,我們在前面加上了 await 這個關鍵字,可以從執行結果看到明顯的差異,await 發揮了等待非同步執行結果的效用,注意 OnButtonClick 因為內部使用了 await 這個 Keyword 所以也必須被加上 async 作修飾。

private async void OnButtonClick(object sender, RoutedEventArgs e)
{
    lbl.Text = "Button Click Start\n" + DateTime.Now.ToString("yyyy/MM/dd - hh:mm:ss") + "\n\n";
    await Delay();
    lbl.Text = lbl.Text + "Button Click End\n" + DateTime.Now.ToString("yyyy/MM/dd - hh:mm:ss") + "\n\n";
}

private async Task Delay()
{
    lbl.Text = lbl.Text + "Delay Start\n" + DateTime.Now.ToString("yyyy/MM/dd - hh:mm:ss") + "\n\n";
    await Task.Delay(1500);
    lbl.Text = lbl.Text + "Delaying\n" + DateTime.Now.ToString("yyyy/MM/dd - hh:mm:ss") + "\n\n";
    await Task.Delay(1500);
    lbl.Text = lbl.Text + "Delay End\n" + DateTime.Now.ToString("yyyy/MM/dd - hh:mm:ss") + "\n\n";
}

// 輸出結果
Button Click Start
2012/11/30 - 23:28:34

Delay Start
2012/11/30 - 23:28:34

Delaying
2012/11/30 - 23:28:36

Delay End
2012/11/30 - 23:28:37

Button Click End
2012/11/30 - 23:28:37

第五個範例,我們將 Delay 回傳值修改為 Task<String> 代表這個非同任務會回傳字串,在按鍵事件中我們也使用 await 來接非同步方法的回傳值,可以看到輸出的順序和第四個範例一樣,但是輸出步驟有差異,除了 Button Click Start 之外的四行幾乎是同時印出來的,因為我們把 Delay 內的資訊組為字串回傳給按鍵事件做輸出。

private async void OnButtonClick(object sender, RoutedEventArgs e)
{
    lbl.Text = "Button Click Start\n" + DateTime.Now.ToString("yyyy/MM/dd - hh:mm:ss") + "\n\n";
    String strResult = await Delay();
    lbl.Text = lbl.Text + strResult + DateTime.Now.ToString("yyyy/MM/dd - hh:mm:ss") + "\n\n";
    lbl.Text = lbl.Text + "Button Click End\n" + DateTime.Now.ToString("yyyy/MM/dd - hh:mm:ss") + "\n\n";
}

private async Task<String> Delay()
{
    String strRes = "";
    strRes = strRes + "Delay Start\n";
    await Task.Delay(1500);
    strRes = strRes + "Delaying\n";
    await Task.Delay(1500);
    strRes = strRes + "Delay End\n";
    return strRes;
}

// 輸出結果
Button Click Start
2012/11/30 - 23:28:41

Delay Start
Delaying
Delay End
2012/11/30 - 23:28:44

Button Click End
2012/11/30 - 23:28:44

可以發現將傳統非同步 + 委派處理的流程修改為 async / await 方法是非常容易的,async 可以很隨性的使用,任何匿名、委派、甚至 override 的方法都可以加上去,例如以下範例顯示我們可以加上 async 延伸出包含非同步方法的子類別,非常的方便好用,真的會上癮。

public abstract class BaseTask
{
    public abstract void Run1();
    public abstract void Run2();
    public abstract Task<String> Run3();
}

public class SubTask : BaseTask
{
    public override void Run1()
    {
    }

    public override async void Run2()
    {
        await Task.Delay(3000);
    }

    public override async Task<String> Run3()
    {
        // return "Ascii"; // 可以直接回傳字串
        return await Task<String>.Factory.StartNew(() => { return "Ascii"; }); // 也可以調用非同步方法
    }
}

1 則留言: