[ C# ] クリックイベントを非同期で実行して画面のロックを防止する ( async / await )

Pocket

ここでは、Visual Studio 2012 ( .NET Framework 4.5 ) から利用できるようになった async / await を使用して非同期処理のサンプルコードを掲載しています。非同期処理はとても GUI と相性が良いと感じられると思います(感じられないかもしれませんが)。

スポンサーリンク


フォーム画面全体が固まらない非同期処理のサンプル

通常、ボタンクリックイベントなどで時間のかかる処理を行うと、すべての処理が完了するまで画面全体がロックされてしまいます。時間のかかる処理を非同期で行うことで、画面のロックを防ぐサンプルコードになります。

// ボタンクリックイベント
// 非同期処理が実装してあるメソッドには async を記述する必要がある
private async void button1_Click(object sender, EventArgs e)
{
    // ボタン無効化
    this.button1.Enabled = false;

    // 非同期で実行したい処理(Func デリゲート)
    // サンプルは string だが、int型などにも変更できる
    Func<string> LongTimeProc = () =&gt;
    {
        //時間のかかる処理
        for (long l = 0, m = 0; l < 999999999; l++)
        {
            m++; // 特に意味はありません。
        }
        return "OK牧場";
    };

    // 非同期で処理を実行(完了を待機する)
    // 制御がメインスレッドに戻るため、画面全体が固まることはない
    string ret = await Task.Run(LongTimeProc);

    // この位置は非同期処理完了後にメインスレッドで実行される
    // 時間のかかる処理の完了を待機しないという訳ではない(注意)

    // ボタン有効化
    this.button1.Enabled = true;
}

メインスレッドに割り当てる処理を制御する

上記のサンプルコードでは、非同期処理実行後のボタン有効化は元々のメインスレッド上で実行されましたので、正常に終了します。小難しい言葉を使うと、元々のコンテキストにマーシャリングされたという表現を使うようです。

この動作は、フォームのデフォルト動作のようですが、非同期処理実行後の処理(ここでは、ボタンの有効化)をメインスレッドで実行しないように制御することもできます。ただし、上記のサンプルコードでは、別スレッドからボタン操作を行うことになりますので System.InvalidOperationException 例外が発生します。

// 非同期処理実行後の部分はメインスレッドで実行しないようにする
string ret = await Task.Run(LongTimeProc).ConfigureAwait(false);

// ボタン有効化
this.button1.Enabled = true;

/* 
 * ボタン有効化で発生する例外
 * System.InvalidOperationException
 * ----------------------------------------------------------------------------------
 * 有効ではないスレッド間の操作: コントロールが作成されたスレッド以外のスレッドから
 * コントロール 'button1' がアクセスされました。
 */

余計なことはしないという感覚で私はいます

非同期処理をキャンセルする

時間がかかるとの理由でわざわざ非同期処理としているのですから、非同期処理をキャンセルしたい場合も当然あります。以下に、上記のサンプルにキャンセル処理を追加したコードを掲載しています。詳細はコメントを参照ください。特に変更のある部分にコメントを追加してあります。

// キャンセル用 ( using System.Threading; )
private CancellationTokenSource _cancelToken;

// ボタンクリックイベント
// 非同期処理が実装してあるメソッドには async を記述する必要がある(VSが教えてくれます)
private async void button1_Click(object sender, EventArgs e)
{
    this.button1.Enabled = false;

    // キャンセル用トークンを生成
    _cancelToken = new CancellationTokenSource();

    Func<string> LongTimeProc = () =>
    {
        try
        {
            //時間のかかる処理
            for (long l = 0, m = 0; l < 999999999; l++)
            {
                // キャンセルされていれば例外を発生させる
                // System.OperationCanceledException 例外が発生する
                _cancelToken.Token.ThrowIfCancellationRequested();
                m++;
            }
            return "OK牧場";
        }
        catch (Exception ex)
        {
            // 例外を補足する
            Console.WriteLine(ex.Message); // 操作は取り消されました。
            return String.Empty;
        }
    };
    // 非同期処理を実行する
    string ret = await Task.Run(LongTimeProc);
	
    this.button1.Enabled = true;
}

// キャンセル用のイベントハンドラ
private void button2_Click(object sender, EventArgs e)
{
    // 非同期処理をキャンセルする.
    if(_cancelToken != null)
    {
        _cancelToken.Cancel();
    }
}

非同期処理内でボタンなどのコントロールを直接操作するとやはり例外が発生しますので、注意が必要です。ただし、期待する動作をする場合もあるようなので、潜在的なバグとして潜り込むこともあるかもしれません。これも注意するポイントです。

GUI との相性が非常に良いように感じられましたでしょうか?私は感じました。

スポンサーリンク

Pocket

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>