C#でのasync&awaitとTaskの使い方と非同期の考え方をわかりやすく解説#4

当サイトではアフィリエイト広告を利用しています。

C#での非同期プログラミング

Task編

それでは続いてTaskを使った非同期プログラミングを解説します。.NETFramework4.0以降の非同期処理は基本的にはTaskを使うと思っておいてよいでしょう。

Taskとは何か?

.NETFramework4.0で登場したTaskクラスは,以前のThreadPoolの機能をプログラマーにとって書きやすいように提供されたものです。原理は昔からあるスレッドと同じですが,より人間に扱いやすい形に書けることから,Taskを使うのがよいでしょう。

Taskを使うとどんな良いことがあるのか?

機能的にはThreadPoolと同じですが,人間が見て理解しやすいようにコーディングができるようになったことと,後述するasync&awaitと組み合わせて使うことにより,同期プログラミング的に実装が可能になり,可読性がとてもよくなります。

ThreadPoolで使用できる非同期メソッドは戻り値がvoid,引数がobjectのパターンが1つでしたが,Taskの場合は自由な引数と戻り値を指定できるため,同期処理と同じ感覚でメソッドの呼び出しができます。また,キャンセル処理などもサポートされています。

Taskを使わないとどんな悪いことがあるのか?

Taskを使わずにThreadPoolを使ってもパソコン的な負荷は同じですが,コーディングのしやすさ,可読性のよさでTaskを使うのがベストな選択となります。

Taskの使い方

それでは前述の5秒かかる検索処理プログラムをTaskを使って実装してみます。

public partial class ThreadForm4 : Form
{
    private int _count = 0;
    public ThreadForm4()
    {
        InitializeComponent();
    }

    private void CountButton_Click(object sender, EventArgs e)
    {
        this.CountButton.Text = _count++.ToString();
    }
  
    /// <summary>
    /// Taskボタンクリック
    /// </summary>
    /// <param name="sender">コントロール</param>
    /// <param name="e">イベント引数</param>
    private void TaskButton_Click(object sender, EventArgs e)
    {
        var context = TaskScheduler.FromCurrentSynchronizationContext();
        Task.Run(() => GetDataAsync()).ContinueWith(dtos =>
        {
            dataGridView1.DataSource = dtos.Result;
        }, context);
    }

    /// <summary>
    /// 検索処理
    /// </summary>
    /// <returns></returns>
    private List<DTO> GetDataAsync()
    {
        var result = new List<DTO>();
        for (int i = 0; i < 5; i++)
        {
            System.Threading.Thread.Sleep(1000);
            result.Add(new DTO(i.ToString(), DateTime.Now.ToString("HH:mm:ss")));
        }

        return result;
    }
}
TaskButton_Clickイベント
        var context = TaskScheduler.FromCurrentSynchronizationContext();
        Task.Run(() => GetDataAsync()).ContinueWith(dtos =>
        {
            dataGridView1.DataSource = dtos.Result;
        }, context);

TaskButtonのクリックイベントにはいろいろ書かれていますが,まず「Task.Run(() => GetDataAsync())」の部分に注目してください。このTask.RunメソッドでGetDataAsyncメソッドを呼び出しています。ここまでは前述のThreadPoolと特に代わりがありません。しかし,Task.Runの引数に設定できる項目を確認してみるとわかると思いますが,Task.Runに指定できる戻り値や引数はある程度自由に設定できます。

今回の例で言うとこれまで戻り値がvoidだったGetDataAsync関数の戻り値はList<DTO>になっています。前述の同期処理のとき意外は常に戻り値を返却することができませんでしたが,Taskでは可能になりました。

これまで戻り値が指定できない理由として,非同期メソッド呼出し後は,クリックイベントはすぐに完了してしまうため,戻り値を受けるすべがありませんでしたが,Taskでは非同期処理が完了後に行う処理を書くことができます。それにより,戻り値を受け取った後の,画面操作をボタンクリックイベントに記述することが可能です。

それがContinueWithで続く部分です。ContinueWithに書かれている部分が非同期処理を終えた後に動作する処理になります。dtosは非同期の戻り値で,中括弧{}内はいつもどおりのデータグリッドへの値のセット処理が書かれています。dtos.Resultに非同期処理の戻り値が入っています。

contextは1行目のTaskScheduler.FromCurrentSynchronizationContext()ですが,これは,UIスレッド上で画面操作をするためのもので,ThreadPoolのときのthis.Invokeに相当するものです。少々ややこしく見えますが,ボタンクリックイベントに「検索処理」と「データグリッドへの設定」が書けるようになったため,処理の順番どおりの記述ができるという意味では,同期処理と同じ感覚でコードを書けるため,よりコーディングが自然になったといえます。

GetDataAsync関数

GetDataAsyncは,ThreadPoolの時とは異なり,戻り値を返すようになり,またUIスレッドへ戻して画面操作するという処理を行う必要がなくなりました。それにより,本当に非同期で行いたいものだけを記述することになるため,より自然で,コーディング形になっています。

Taskの使い方まとめ

このようにTaskを使うと同期プログラミングのときと同じような処理の順番で書くことができ,さらにThreadPoolの原理を使いながら,ThreadPoolよりはるかに使いやすくなっています。とはいえFromCurrentSynchronizationContextのあたりが実装し辛い気がしますよね?そのあたりは次のasync&awaitですべて解決されて,より同期的に書きやすくなっていますのでそちらを参照してください。