非同期処理でUIスレッドを操作する方法

 非同期処理でUIスレッドを操作するやり方について、ボタン button1 とテキストボックス textbox1 のコントロールが配置された Form で、button1 のクリックする(button1_Click)と、並列処理した結果を非同期的に textbox1 へ表示するいくつかのソースコード例を簡単なサンプルとして示します。

下準備

 button1 のクリックイベントハンドラに紐づけされた button1_Click メソッドと、結果表示するために文字列を整形する GetString メソッドを用意します。
 なお、button1_Click メソッドの非同期処理を行うタスク内には、並列ループ処理を行う Parallel.For メソッドがありますが、下記のように lock ステートメントと同期オブジェクトを使って、ロック状態でクリティカルセクション(複数のスレッドが同時に操作してはダメな部分)を処理しないと、textbox1 へ不完全な状態で結果出力されてしまいます。

private void button1_Click(object sender, EventArgs e)
{
    textBox1.Text = this.GetString(Thread.CurrentThread.ManagedThreadId, "NomalCall");

    var task = Task.Factory.StartNew(() =>
    {
        var syncObject = new Object();
        var strBuilder = new StringBuilder();

        Parallel.For<string>(0, 10, () => "",
            (i, loop, str) => str += this.GetString(Thread.CurrentThread.ManagedThreadId, "Paralell.For", i),
            x =>
            {
                lock (syncObject)
                {
                    strBuilder.Append(x);
                }
            });

        return strBuilder.ToString();
    });
}

private string GetString(int threadID, string prefix, int index = -1, string append = "")
{
    var basedFormat = new StringBuilder("ThreadID= {2}\r\n");

    basedFormat.Insert(0, (index >= 0) ? "{3}: index= {1}, " :
        string.IsNullOrEmpty(append) ? "{3}: UI " : "{0}\r\n{3}: UI ");

    return string.Format(basedFormat.ToString(), append, index, threadID, prefix);
}

ContinueWith メソッドを使用する方法

 下準備した button1_Click メソッド内の最後尾(task 変数の宣言の下)に以下のソースコードを追加します。

var taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();

task.ContinueWith(t =>
{
    textBox1.AppendText(this.GetString(
        Thread.CurrentThread.ManagedThreadId, "ContinueWith", append: t.Result));
}, taskScheduler);

この状態で実行すると以下のような結果になります。

NomalCall: UI ThreadID= 10
Paralell.For: index= 1, ThreadID= 12
Paralell.For: index= 5, ThreadID= 12
Paralell.For: index= 7, ThreadID= 12
Paralell.For: index= 9, ThreadID= 12
Paralell.For: index= 0, ThreadID= 11
Paralell.For: index= 2, ThreadID= 11
Paralell.For: index= 3, ThreadID= 11
Paralell.For: index= 4, ThreadID= 11
Paralell.For: index= 6, ThreadID= 11
Paralell.For: index= 8, ThreadID= 11

ContinueWith: UI ThreadID= 10

 結果から見ても分かるように、ContinueWithメソッドで実行されるタスクは、UI スレッドで実行されています。なぜ UI スレッドで実行されるのかというと、TaskScheduler クラスの FromCurrentSynchronizationContexttaskScheduler メソッドの戻り値を、ContinueWith メソッドに渡すことで、コントロールにアクセスした時に UI スレッド上で処理を行うように設定されるためです。上記例では、textbox1 へアクセスした時に UI スレッド上で処理を実行します。
 メリットは、次に紹介する Control.Invoke メソッドを使用する方法よりも疎結合(他のオブジェクト等との依存度が低い)で自由度が高いということ、一方、デメリットは、次の方法と比べてコーディング量が少し増えてしまうことです。

Control.Invoke メソッドを使用する方法

 下準備した button1_Click メソッドの task 変数内の最後の行(「return strBuilder.ToString();」の行)を以下のソースコードに置き換えます。

textBox1.Invoke((Action)(() =>
{
    textBox1.AppendText(this.GetString(
        Thread.CurrentThread.ManagedThreadId, "textBox1.Invoke", append: strBuilder.ToString()));
}));

この状態で実行すると以下のようになります。

NomalCall: UI ThreadID= 9
Paralell.For: index= 1, ThreadID= 11
Paralell.For: index= 8, ThreadID= 11
Paralell.For: index= 0, ThreadID= 10
Paralell.For: index= 2, ThreadID= 10
Paralell.For: index= 3, ThreadID= 10
Paralell.For: index= 4, ThreadID= 10
Paralell.For: index= 5, ThreadID= 10
Paralell.For: index= 6, ThreadID= 10
Paralell.For: index= 7, ThreadID= 10
Paralell.For: index= 9, ThreadID= 10

textBox1.Invoke: UI ThreadID= 9

 UI スレッドの非同期処理で昔からよく使われる方法です。
 Control クラスから継承される Invoke メソッドを使用すると、メソッド内で実行されるコントロールに対しての全ての処理は、UI スレッド上で行われます。
 メリットは、ContinueWith メソッドを使用するよりも、コーディング量が少なく理解しやすいソースコードになることですが、デメリットは、task があるクラスと ui を操作するクラスが分けられている場合には少々手間なことをしないといけないため扱いにくいことです。
 また、この方法を使った場合で Parallel.For メソッドのみで非同期処理をしている時に陥りがちな落とし穴があり、Parallel.For メソッド内で Invoke メソッドを呼び出すとデッドロックが発生します。理由としては、Parallel.For メソッドを呼び出すと実行が終わるまでは呼び出し元スレッドがブロックされてしまうためです。従って、デッドロックを引き起こさないようにするためには、別スレッドで実行する機構を持つ Task や Thread の内部に Invoke メソッドを含む Parallel.For を配置します。なお、上記例ではその対策がされているためデッドロックを引き起こすことはありません。

まとめ

纏め。どちらも一長一短なのでTPOに合わせて適した方を使うのがベスト。

  • ContinueWith メソッドのメリットとデメリット
    メリット:疎結合で自由度が高い
    デメリット:コーディング量が多くなりがち
  • Control.Invoke メソッドのメリットとデメリット
    メリット:コーディング量が小さく、可読性が高め
    デメリット:コントロールとの結合度が高い、単体のParallel.Forと相性が悪い