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

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

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

Threadクラス編

それでは最初にThreadクラスを用いた実装方法を紹介します。このThreadクラスは.NETFramework1.1時代から存在する,最も古い書き方ですが,スレッドの動作を理解するにはもっとも例になります。それでは先ほどの検索に5秒かかるサンプルをThreadクラスを使用して実装した例を次に示します。

【画面】

新たにThreadボタンを追加しました。このボタンを押下すると,非同期で5秒かかる検索処理をします。5秒間の検索中もCountボタンを押下すると,カウントアップしてTextに表示されます。

検索後は,検索結果が一覧に表示されます。これは同期プログラミングでも同じですね。途中の検索中にCountボタンが押下できるか?というのが決定的な違いになります。

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

    private void CountButton_Click(object sender, EventArgs e)
    {
        this.CountButton.Text = _count++.ToString();
    }

    /// <summary>
    /// Threadボタン
    /// </summary>
    /// <param name="sender">コントロール</param>
    /// <param name="e">イベント引数</param>
    private void ThreadButton_Click(object sender, EventArgs e)
    {
        var t = new Thread(GetDataAsync);
        t.Start();
    }

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

        this.Invoke((Action)delegate ()
        {
            dataGridView1.DataSource = dtos;
        });
    }
}
ThreadButton_Clickイベント

Threadボタンを押下すると,Threadクラスを生成しています。

        var t = new Thread(GetDataAsync);
        t.Start();

Threadクラスの引数には非同期で処理したい関数を指定します。
t.Start()で非同期処理(GetDataAsync)がスタートします。注意していただきたいのは,t.Start()はGetDataAsyncを呼び出した後,GetDataAsyncの完了を待たずに処理を続けます。ですからt.Start()の次の行にMessageBox.Show(“完了しました”);などと記述すると,検索処理が開始した時点でメッセージボックスが表示され,完全に誤ったメッセージが表示されてしまいます。

GetDataAsync処理

GetDataAsyncでは,5秒間かかる検索処理を行います(サンプルでは5秒間ウエイトしてデータを返しているだけで実際の検索はしていません)。

GetDataの語尾にAsyncと書いているのは,非同期で呼び出される関数に付ける命名規則の慣例です。この関数は非同期で呼び出されることを想定して実装されているということを実装者に伝えるために書いていますので,この慣例に従った命名にするのが望ましいでしょう。非同期で動作しているスレッドでは画面のコントロールを操作してはいけないという規則があるため,非同期で動作しているスレッドなのか,UIスレッド上からのみ呼び出されるのかを識別できたほうが,プログラマーにとっては理解しやすくなり,不具合の混入を防ぐことができます。

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

    this.Invoke((Action)delegate ()
    {
        dataGridView1.DataSource = dtos;
    });
}

同期プログラミングとの違いはまず,戻り値がvoid(戻り値なし)になっていること。
Threadクラスは戻り値を指定できないため,戻り値がvoidの関数を呼び出す必要があります。

dtosの生成や5秒間のウエイト処理は同じ。最後のthis.Invokeから始まる4行が今回初めて出てきました。やりたいこと自体は「dataGridView1.DataSource = dtos;」をしたいだけです。要するにデータグリッドのデータソースにdtosをセットしたいということです。でもこのコードを「dataGridView1.DataSource = dtos;」とだけ記述すると実行時エラーとなります。

実行時エラーになる理由

実行時エラーになる理由は,ワーカースレッド上で画面のコントロールを操作してはいけないため。画面のコントロールの操作はUIスレッドでしか行えないというC#の決まりごとがあるため,新たに生成したスレッドから画面のコントロールを操作することができません。

ワーカースレッドから画面コントロールを操作する方法

UIスレッド上でしか画面コントロールの操作が許可されていないため,ワーカースレッドから画面コントロールを操作するには,UIスレッドに戻してあげる必要があります。そのUIスレッドい戻す方法が,this.Invokeで始まる部分です。「this.Invoke((Action)delegate ()」で始まる中括弧{}の中を通っている間は,別スレッドは中断され,UIスレッドに処理が戻ります。その時のみ画面コントロールの操作が許されます。

非同期処理を行う場合は常にこの問題はつきまといます。別スレッド上で動作しているのかUIスレッド上で動作しているのかを意識して,別スレッド上で動作している所では上記方法でUIスレッドに戻して画面コントロールを操作してください。

.NETFrameworkが4.0以降であれば,Taskが使用できるため,後述する別の書き方でUIスレッドに戻すことができます。また,.NETFrameworkが4.5以降であればasync&awaitが使用できるため,もっと簡単な方法で記述することができます。詳細は後述します。

Threadクラスのまとめ

Threadクラスを使用すると,時間のかかる処理を別の関数に記述し,そこで処理している間は画面操作が可能となる。別スレッドから画面コントロールを処理する場合は,this.InvokeでUIスレッドに戻す。

以上のような点に注意することで,非同期プログラミングが可能となります。