複数のTaskのすべての終了を待つ - C#

C#で複数のTaskの終了を待つ方法を紹介します。

概要

以下の記事では、Taskを利用したシンプルなコードを紹介しました。同期処理版のコードではResultを利用してTaskの終了を待ち結果を取得し、 非同期版ではawaitを利用してTaskの完了を待ちました。この記事では複数のTaskのすべての完了を待つコードを紹介します。

プログラム1 : Resultを利用した例

TaskクラスのResultプロパティはTaskの完了を待つため、複数のTaskに対してResultプロパティを取得すれば、すべてのTaskの完了を検出できます。

UI

Windows Formアプリケーションを作成します。下図のフォームを作成します。
今回のプログラムでは [button1]のみ利用します。

コード

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Windows.Markup;

namespace WaitTask
{
  public partial class FormWaitTask : Form
  {
    public FormWaitTask()
    {
      InitializeComponent();
    }

    private void button1_Click(object sender, EventArgs e)
    {
      List<int> value1 = new List<int>(new int[] { 3, 5, 6, 7 });
      List<int> value2 = new List<int>(new int[] { 1, 8, 4, 5, 9, 7 });
      List<int> value3 = new List<int>(new int[] { 2, 10, 5, 8, 1, 4 });

      Task<int> t1 = Task.Factory.StartNew(() => Proc(value1));
      Task<int> t2 = Task.Factory.StartNew(() => Proc(value2));
      Task<int> t3 = Task.Factory.StartNew(() => Proc(value3));

      textBox1.Text += "すべてのタスクの処理が完了しました。"
        + string.Format("t1={0:d}, t2={1:d}, t3={2:d}", t1.Result, t2.Result, t3.Result);
    }

    private int Proc(List<int> values) {
      Thread.Sleep(3000);

      int result = 0;
      foreach (int v in values) {
        result += v;
      }

      return result;
    }
  }
}

解説

Taskを3つ作成し、実行しています。Taskでの処理はパラメーターとして渡されたintのリストの値の合計を計算して戻り値として返す処理を実行しています。
Task実行で待機しないため3つのタスクは同時に処理されます。

画面表示のタイミングで、3つのTaskオブジェクトのResultプロパティを取得しており、このタイミングで待機状態になります。待機中はメインスレッドがブロックされるため、 メインウィンドウの操作ができなくなり画面がフリーズしたような動作になります。

実行結果

プロジェクトを実行します。下図のウィンドウが表示されます。


[button1]をクリックします。3秒ほどすると下図のメッセージが表示されます。3秒で結果が出るため、3つのタスクが並列で処理していることが確認できます。 また、メッセージが表示されるまでの間、メインウィンドウがフリーズ状態になることも確認できます。

プログラム2 : WaitAllで完了を待つ場合

Resultプロパティを参照しない、または参照できない場合は、WaitAllメソッドを利用してTaskの完了を待つことができます。

UI

Windows Formアプリケーションを作成します。下図のフォームを作成します。
このプログラムでは [button2]のみ利用します。

コード

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Windows.Markup;

namespace WaitTask
{
  public partial class FormWaitTask : Form
  {
    public FormWaitTask()
    {
      InitializeComponent();
    }

 private void button2_Click(object sender, EventArgs e)
    {
      List<int> value1 = new List<int>(new int[] { 3, 5, 6, 7 });
      List<int> value2 = new List<int>(new int[] { 1, 8, 4, 5, 9, 7 });
      List<int> value3 = new List<int>(new int[] { 2, 10, 5, 8, 1, 4 });

      Task t1 = Task.Factory.StartNew(() => ProcF(value1));
      Task t2 = Task.Factory.StartNew(() => ProcF(value2));
      Task t3 = Task.Factory.StartNew(() => ProcF(value3));

      fs = new FileStream("result.txt", FileMode.Append, FileAccess.Write, FileShare.Write);
      sw = new StreamWriter(fs, Encoding.ASCII);
      tw = TextWriter.Synchronized(sw);
      Task.WaitAll(new Task[] { t1, t2, t3 });
      tw.Close();
      sw.Close();
      fs.Close();

      textBox1.Text += "すべてのタスクの処理が完了しました。";
    }

    private void ProcF(List<int> values)
    {
      Thread.Sleep(3000);

      int result = 0;
      foreach (int v in values) {
        result += v;
      }
      tw.WriteLine(result);
    }

  }
}

解説

Taskを3つ作成し、実行しています。Taskでの処理はパラメーターとして渡されたintのリストの値の合計を計算してテキストファイルに結果を書き込む処理を実行します。
Task実行で待機しないため3つのタスクは同時に処理されます。

Taskからの戻り値が無いため、Resultプロパティで待機ができないコードになります。 この場合は、Task.WaitAll()メソッドを利用してTaskオブジェクトの終了を待つことができます。WaitAllメソッドにTaskオブジェクトの配列を与えます。 与えたすべてのTaskオブジェクトが完了すると、次行以降を実行しファイルのクローズ処理をします。

実行結果

プロジェクトを実行します。下図のウィンドウが表示されます。


[button2]をクリックします。3秒ほどすると下図のメッセージが表示されます。3秒で結果が出るため、3つのタスクが並列で処理していることが確認できます。 また、メッセージが表示されるまでの間、メインウィンドウがフリーズ状態になることも確認できます。


実行ファイルと同じ位置に "result.txt" ファイルが作成されます。ファイル内容を確認します。パラメーターで与えられた数値のリストの合計値の結果がテキストファイル中に 書き込まれていることが確認できます。

プログラム3 : 非同期関数を利用してawaitで完了を待つ

先の2つのプログラムでTaskを待機できますが、待機時にはメインスレッドがブロックされるため、待機中はウィンドウが固まり操作できなくなります。 待機中でもメインスレッドをブロックしない方法として、コールバック関数やあらかじめ設定したメソッドをInvokeで呼び出す方法がありますが、新しいC#では 非同期関数を利用することでメインスレッドをブロックせずに待機するコードをシンプルに記述できます。

UI

Windows Formアプリケーションを作成します。下図のフォームを作成します。
このプログラムでは [button3]のみ利用します。

コード

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Windows.Markup;

namespace WaitTask
{
  public partial class FormWaitTask : Form
  {
    public FormWaitTask()
    {
      InitializeComponent();
    }

    private async void button3_Click(object sender, EventArgs e)
    {
      List<int> value1 = new List<int>(new int[] { 3, 5, 6, 7 });
      List<int> value2 = new List<int>(new int[] { 1, 8, 4, 5, 9, 7 });
      List<int> value3 = new List<int>(new int[] { 2, 10, 5, 8, 1, 4 });

      Task<int> t1 = Task.Factory.StartNew(() => Proc(value1));
      Task<int> t2 = Task.Factory.StartNew(() => Proc(value2));
      Task<int> t3 = Task.Factory.StartNew(() => Proc(value3));

      int[] results = await Task.WhenAll<int>(new Task<int>[] { t1, t2, t3 });

      textBox1.Text += "すべてのタスクの処理が完了しました。"
        + string.Format("t1={0:d}, t2={1:d}, t3={2:d}", results[0], results[1], results[2]);

    }

    private int Proc(List<int> values) {
      Thread.Sleep(3000);

      int result = 0;
      foreach (int v in values) {
        result += v;
      }

      return result;
    }
  }
}

解説

Task.WaitAll() メソッドは戻り値のない void メソッドのため、await で待機できません。非同期関数で待機する場合は、 Task.WhenAll() メソッドを利用します。WhenAllメソッドの第一引数に終了を待つTaskオブジェクトの配列を与えます。また、WhenAllメソッドは Taskオブジェクトの戻り値をまとめて返すこともできます。WhenAllメソッドには Task<TResult[]>を戻り値にとるものがありますので、 完了を待つTaskオブジェクトの戻り値がすべて同じ型であれば、戻り値の型の配列ですべてのTaskオブジェクトの結果を受け取れます。

実行結果

プロジェクトを実行します。下図のウィンドウが表示されます。


[button3]をクリックします。3秒ほどすると下図のメッセージが表示されます。3秒で結果が出るため、3つのタスクが並列で処理していることが確認できます。 また、非同期関数で待機するため、メッセージが表示されるまでの間、メインウィンドウはフリーズせず、ウィンドウ移動などの操作ができます。

参考 : Invokeメソッドを利用してメインスレッドをブロックしない例

スレッドの待機はしませんが、Taskでの処理中に、メインウィンドウをフリーズさせたくない場合の実装例を紹介します。
先に紹介した非同期関数を利用する方法が新しいC#では利用できますが、以前のバージョンの場合は、Invokeメソッドを利用して、メインスレッドのメソッドを呼び出すことで、 処理の完了を検出できます。
Invokeメソッドの動作に関する詳細はこちらの記事も参照して下さい。

UI

Windows Formアプリケーションを作成します。下図のフォームを作成します。
このプログラムでは [button4]のみ利用します。

コード

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Windows.Markup;

namespace WaitTask
{
  public partial class FormWaitTask : Form
  {
    public FormWaitTask()
    {
      InitializeComponent();
    }

    private void button4_Click(object sender, EventArgs e)
    {
      List<int> value1 = new List<int>(new int[] { 3, 5, 6, 7 });
      List<int> value2 = new List<int>(new int[] { 1, 8, 4, 5, 9, 7 });
      List<int> value3 = new List<int>(new int[] { 2, 10, 5, 8, 1, 4 });

      Task t1 = Task.Factory.StartNew(() => ProcV(value1));
      Task t2 = Task.Factory.StartNew(() => ProcV(value2));
      Task t3 = Task.Factory.StartNew(() => ProcV(value3));
    }

    public delegate void MyHandler(int result);

    private void cbf(int result)
    {
      textBox1.Text += (int)result +", ";
    }

    private void ProcV(List<int> values)
    {
      Thread.Sleep(3000);

      int result = 0;
      foreach (int v in values) {
        result += v;
      }

      this.Invoke((MyHandler)cbf, result);

      /*
      //この記述でもOK
      MyHandler mh = cbf;
      this.Invoke(mh, result);
      */
    }
  }
}

解説

メインスレッドでは Taskオブジェクトを作成し実行開始しますが、終了は待機しません。
それぞれのTaskは処理を実行し、実行かが完了すると、メインスレッドでcbf() メソッドを呼び出し実行します。サブスレッドから直接メインスレッドを呼び出すと、 スレッドセーフではないため、メインウィンドウのUI操作ができないため、Invokeメソッドを利用して、メインスレッドでcbfメソッドを実行するようにします。
cbfメソッドでは、テキストボックスに処理結果を表示します。
なお、すべてのタスクが終了したか確認したい場合は、cbfメソッドはメインスレッドで実行されスレッドセーフであり並列実行はされないため、 cbfメソッド呼び出し時にカウントアップするなどの方法で検出ができます。 今回の場合、3回目の呼び出し時点で、3つのTaskオブジェクトのすべての処理が完了したことが判定できます。

実行結果

プロジェクトを実行し、[button4]をクリックします。3秒ほど経過するとテキストボックスに結果が表示されます。
なお、処理が完了した順にテキストボックスに値が表示されるため、結果の数値が表示される順番は毎回異なります。
("34, 21, 30, " と表示される場合や、"34, 30, 21," と表示されることもあります。)

参考 : WaitAll中にメインスレッドにInvokeすることで、無限ブロックになる例

WaitAllでメインスレッドがブロックしている最中に、メインスレッドのメソッドをInvokeで呼び出すと、メインスレッドがブロックされているため、Invoke先のメソッドも実行されず、 すべての処理がブロックされ、無限に待ち続けてしまいアプリケーションがフリーズします。WaitAllで待機している場合はメインスレッドをInvokeメソッドで呼び出せないので注意が必要です。

UI

Windows Formアプリケーションを作成します。下図のフォームを作成します。
このプログラムでは [button5]のみ利用します。

コード

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Windows.Markup;

namespace WaitTask
{
  public partial class FormWaitTask : Form
  {
    public FormWaitTask()
    {
      InitializeComponent();
    }

    private void button5_Click(object sender, EventArgs e)
    {
      List<int> value1 = new List<int>(new int[] { 3, 5, 6, 7 });
      List<int> value2 = new List<int>(new int[] { 1, 8, 4, 5, 9, 7 });
      List<int> value3 = new List<int>(new int[] { 2, 10, 5, 8, 1, 4 });

      Task t1 = Task.Factory.StartNew(() => ProcNG(value1));
      Task t2 = Task.Factory.StartNew(() => ProcNG(value2));
      Task t3 = Task.Factory.StartNew(() => ProcNG(value3));

      Task.WaitAll(new Task[] { t1, t2, t3 });

      textBox1.Text += "すべてのタスクの処理が完了しました。";

    }

    private void ProcNG(List<int> values)
    {
      Thread.Sleep(3000);

      int result = 0;
      foreach (int v in values) {
        result += v;
      }

      this.Invoke((MethodInvoker)(()=>{ textBox1.Text += result.ToString() + ", "; }));
    }
  }
}

解説

メインスレッドでTaskオブジェクトの実行後に Task.WaitAll() メソッドで待機します。待機中はメインスレッドはブロック状態になります。
Task側では処理の完了後にInvokeメソッドを実行して、TextBoxにメッセージを表示する処理を実行しますが、メインスレッドがブロックされているため、 処理は実行できず処理が止まってしまいます。
すべての処理が止まってしまい、Taskの完了も発生しないため、デッドロック状態となってしまいます。

実行結果

プロジェクトを実行し、メインウィンドウの[button5]をクリックします。アプリケーションがフリーズしてしまうことが確認できます。

著者
iPentec.com の代表。ハードウェア、サーバー投資、管理などを担当。
Office 365やデータベースの記事なども担当。
最終更新日: 2021-08-24
作成日: 2020-02-05
iPentec all rights reserverd.