C#でasync/awaitを使った非同期処理

C#

.NET Framework 4.5(C#5.0)(Visual Studio 2012)からasync/awaitという構文が追加されました。
この構文は非同期な並列処理を簡単にコーディングする事ができます。

async/awaitを使った非同期処理(マルチスレッド)についてまとめてみようと思います。



非同期処理(マルチスレッド)でないとどうなるのか

以下のコードはスタートボタンが押されると重たい処理を実行し、キャンセルボタンでその処理を停止させようとした例ですがうまくいきません。

同期的な処理の例


public partial class Form1 : Form {
    private bool m_Cancel;

    public Form1() {
        InitializeComponent();
    }

    //ボタンを押された時に重たい処理をする
    //直列処理の場合
    private void btnStart_Click(object sender, EventArgs e) {
        this.btnStart.Enabled = false;
        this.btnCancel.Enabled = true;

        m_Cancel = false;
        for (int i = 1; i <= 10; ++i) {
            //キャンセルの処理
            //他の処理が動かない為m_Cancelがtrueになる事は無い
            if (m_Cancel) break;

            //1秒に1回TextBoxへ表示する処理
            //他の処理が動かない為TextBoxの表示も変わらず完全に固まる
            this.textboxProc1.Text = "処理:(" + i + "/10)";
            Thread.Sleep(1000);
        }

        this.btnCancel.Enabled = false;
        this.btnStart.Enabled = true;
    }

    private void btnCancel_Click(object sender, EventArgs e) {
        m_Cancel = true;
    }
}

スタートボタンがクリックされると処理が開始されます。
しかし処理が実行している最中にはボタン操作が一切効きません。処理の中で進捗状況をテキストボックスへ表示させようとしていますが(22行目)それも反映されません。

全ての処理が直列に実行されるため、処理が終わるまでボタンをクリックしたりテキストボックスの表示を書き換えたりといった処理は動作しないことになります。


同期的な処理の例 (DoEventsあり)


public partial class Form1 : Form {
    private bool m_Cancel;

    public Form1() {
        InitializeComponent();
    }

    //ボタンを押された時に重たい処理をする
    //直列処理の場合(DoEvents有り)
    private void btnStart_Click(object sender, EventArgs e) {
        this.btnStart.Enabled = false;
        this.btnCancel.Enabled = true;

        m_Cancel = false;
        for (int i = 1; i <= 10; ++i) {
            //キャンセルの処理
            //DoEvents()によりキャンセルは可能に
            if (m_Cancel) break;

            //1秒に1回TextBoxへ表示する処理
            //DoEvents()によりTextBoxの表示が変わるように
            this.textboxProc1.Text = "処理:(" + i + "/10)";
            Thread.Sleep(1000);

            //DoEvents()により他のウインドウメッセージが処理される
            //但し、1秒に毎にしか処理されないので動きがぎこちない
            //また、ウインドウを移動中の時などでは、ここの処理が止まる
            Application.DoEvents();
        }

        this.btnCancel.Enabled = false;
        this.btnStart.Enabled = true;
    }

    private void btnCancel_Click(object sender, EventArgs e) {
        //DoEvents()によりキャンセルボタンのクリックが可能に
        //但し、1秒に毎にしか処理されないので動きがぎこちない
        m_Cancel = true;
    }
}

最初の例にApplication.DoEventsメソッドを追加してみました。
DoEventsメソッドはメッセージキューにある全てのウインドウズメッセージを実行します。
これにより、メッセージキューにあるボタンクリックやテキストボックスの更新といったものが重たい処理の間に実行されるようになります。

とは言えあくまで同期的な処理です。上記の例では1秒に1回しかDoEventsが呼ばれないのでマウス操作などがぎこちない動きになってしまいます。


非同期処理(マルチスレッド)にする

以下の例はasync/await構文を使って重たい処理の部分を非同期で実行するようになっています。

非同期的な処理の例


public partial class Form1 : Form {
    private CancellationTokenSource m_CancelToken;

    public Form1() {
        InitializeComponent();
    }

    //ボタンを押された時に重たい処理をする
    //async awaitによる並列処理

    //メソッドにasyncを付ける事でawaitが利用可能になる
    private async void btnStart_Click(object sender, EventArgs e) {
        this.btnStart.Enabled = false;
        this.btnCancel.Enabled = true;

        //キャンセル処理にはCancellationTokenSourceを使う
        m_CancelToken = new CancellationTokenSource();

        //awaitで並列処理化
        //並列処理部分を匿名メソッドで定義する
        await Task.Run(() => {
            for (int i = 1; i <= 10; ++i) {
                if (m_CancelToken.IsCancellationRequested) break;

                //非同期で処理されるので直接UIにアクセスできない
                //Invokeをつかって処理する
                this.Invoke((Action)(() => {
                    this.textboxProc1.Text = "処理:(" + i + "/10)";
                }));
                Thread.Sleep(1000);
            }
        });
        //awaitで並列処理部分の処理を待つが、その間他の処理がちゃんと動く

        m_CancelToken.Dispose();
        m_CancelToken = null;

        this.btnCancel.Enabled = false;
        this.btnStart.Enabled = true;
    }

    private void btnCancel_Click(object sender, EventArgs e) {
        //awaitで並列処理部分の処理を待つ間でもここに来る
        if (null != m_CancelToken) {
            m_CancelToken.Cancel();
        }
    }
}

重たい処理の部分は、非同期処理の為のTaskクラスを使って匿名メソッドになっています。(21行目)
このTaskオブジェクトをawait構文によって別スレッドによって非同期に実行しつつTaskの処理が終了するまで待機します。この間メッセージキューに溜まった処理は随時実行されていきます。
awaitを使うメソッドにはasyncを書くのがルールです。(12行目)

このようにasync/awaitを使う事で、コードの見た目は直列的に見やすく書きながら別スレッドで非同期に処理を実行するという事が可能になります。



おまけ

非同期的な処理の例 (複数処理を並列実行)


public partial class Form1 : Form {
    private CancellationTokenSource m_CancelToken;

    public Form1() {
        InitializeComponent();
    }

    //ボタンを押された時に重たい処理をする
    //async awaitによる並列処理

    //メソッドにasyncを付ける事でawaitが利用可能になる
    private async void btnStart_Click(object sender, EventArgs e) {
        this.btnStart.Enabled = false;
        this.btnCancel.Enabled = true;

        //キャンセル処理にはCancellationTokenSourceを使う
        m_CancelToken = new CancellationTokenSource();

        //awaitで並列処理化
        //並列処理部分を匿名メソッドで定義する
        await Task.Run(() => {
            Task task1 = HevyProc(this.textboxProc1);
            Task task2 = HevyProc(this.textboxProc2);
            task1.Wait();
            task2.Wait();

            this.Invoke((Action)(() => {
                this.textboxProc1.Text = "result:" + task1.Result;
                this.textboxProc2.Text = "result:" + task2.Result;
            }));
        });
        //awaitで並列処理部分の処理を待つが、その間他の処理が適切に動く

        m_CancelToken.Dispose();
        m_CancelToken = null;

        this.btnCancel.Enabled = false;
        this.btnStart.Enabled = true;
    }

    private void btnCancel_Click(object sender, EventArgs e) {
        //awaitで並列処理部分の処理を待つ間でもここに来る
        if (null != m_CancelToken) {
            m_CancelToken.Cancel();
        }
    }

    private Task HevyProc(TextBox textbox) {
        return Task.Run(() => {
            for (int i = 1; i <= 10; ++i) {
                // キャンセル要求チェック
                if (m_CancelToken.IsCancellationRequested) return 1;

                //非同期で処理されるので直接UIにアクセスできない
                //Invokeをつかって処理する
                this.Invoke((Action)(() => {
                    textbox.Text = "処理:(" + i + "/10)";
                }));
                Thread.Sleep(1000);
            }
            return 0;
        });
    }
}



コメント