パラメーターと戻り値のある Task (Task<T>) の利用 - C#

パラメーターと戻り値のある Task (Task<T>) のコードを紹介します。

概要

こちらの記事ではTaskクラスを利用したシンプルな並列処理のコードを紹介しました。 Taskによる処理結果はメッセージボックスに表示する動作でしたが、すべての処理が終わった状態でタスクから処理結果の値をメインスレッドで受け取りたい場合があります。 この記事ではTaskからの戻り値を受け取るコードを紹介します。
Taskからの戻り値を受け取るには、TaskクラスのResultプロパティを利用します。

プログラム1 : メソッドを利用してTaskオブジェクトを作成する場合

UI

下図のUIを作成します。ボタンとMultilineプロパティをTrueに設定した複数行テキストボックスを配置します。

コード

下記のコードを記述します。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Threading.Tasks;

namespace SimpleTask
{
  public partial class FormTaskFunctionParam: Form
  {
    public FormTaskFunctionParam()
    {
      InitializeComponent();
    }

    private void button1_Click(object sender, EventArgs e)
    {
      int a = 9, b = 8, c = 7, d = 6;

      List<int> inparam1 = new List<int>();
      inparam1.Add(a);
      inparam1.Add(b);

      Task<int> task1 = new Task<int>(proc, (object)inparam1);

      List<int> inparam2 = new List<int>();
      inparam2.Add(c);
      inparam2.Add(d);

      Task<int> task2 = new Task<int>(proc, (object)inparam2);

      task1.Start();
      task2.Start();

      int result = task1.Result + task2.Result;
      textBox1.Text = result.ToString();

    }

    private int proc(object param)
    {
      System.Threading.Thread.Sleep(2000);
      List<int> inList = (List<int>)param;

      int result=0;
      foreach (int i in inList) {
        result += i;
      }

      return result;
    }
  }
}

解説

Taskに渡すパラメーターを設定します。パラメーターは1つのオブジェクトしか渡せないため、複数の値を渡すためにリストに複数の値を挿入して、リストオブジェクトをタスクに渡す動作にします。下記のコードでリストを作成し値を挿入します。
      List<int> inparam1 = new List<int>();
      inparam1.Add(a);
      inparam1.Add(b);

Taskを作成します。タスクで処理するメソッドにはprocメソッドを与えます。パラメーターには先に作成したリストオブジェクトを与えます。
      Task<int> task1 = new Task<int>(proc, (object)inparam1);

同じ処理を記述して別のタスクも作成します。
      List<int> inparam2 = new List<int>();
      inparam2.Add(c);
      inparam2.Add(d);

      Task<int> task2 = new Task<int>(proc, (object)inparam2);

TaskオブジェクトのStartメソッドを呼び出してタスクの処理を開始します。StartメソッドはTaskの完了を待たずに処理をすぐ戻すため、2つのタスクは(ほぼ)同時に実行開始になります。
    task1.Start();
    task2.Start();

2つのタスクの戻り値をResultプロパティで取得し、結果の合計をテキストボックスに表示します。Resultプロパティの呼び出しでタスクの終了を待つため、Taskの処理が完了しないうちに、Resultプロパティを取得する動作にはなりません。なお、Resultプロパティにアクセスしているこのコードはメインスレッドに記述されているため、結果待ちの間はUIがフリーズしてしまいます。
      int result = task1.Result + task2.Result;
      textBox1.Text = result.ToString();

Taskで処理されるメソッドです。2秒間処理を停止した後、パラメーターとして与えられたリストの値の合計を求め、戻り値として返します。
    private int proc(object param)
    {
      System.Threading.Thread.Sleep(2000);
      List<int> inList = (List<int>)param;

      int result=0;
      foreach (int i in inList) {
        result += i;
      }

      return result;
    }

表示結果

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


[button1]をクリックします。2秒ほど経過すると画面に "30" の文字列が表示されます。int a = 9, b = 8, c = 7, d = 6; で記述した4つの変数の合計値が表示されることが確認できます。
また、ボタンクリック後からテキストボックスに結果が表示されるまでの間、ウィンドウはフリーズしてしまうことも確認できます。これは、Taskの結果を待つResultプロパティがメインスレッドに記述されているためです。

プログラム2 : ラムダ式を利用する場合

UI

下図のフォームを作成します。先のプログラムのフォームにボタンを追加したものです。
今回のプログラムでは [button3] のみを利用します。

コード

下記のコードを記述します。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Threading.Tasks;

namespace SimpleTask
{
  public partial class FormTaskFunctionParam: Form
  {
    public FormTaskFunctionParam()
    {
      InitializeComponent();
    }

    private void button3_Click(object sender, EventArgs e)
    {
      int a = 9, b = 8, c = 7, d = 6;

      List<int> inparam1 = new List<int>();
      inparam1.Add(a);
      inparam1.Add(b);

      List<int> inparam2 = new List<int>();
      inparam2.Add(c);
      inparam2.Add(d);

      Task<int> t1 = new Task<int>((param) => proc(param), inparam1);
      Task<int> t2 = new Task<int>((param) => proc(param), inparam2);

      t1.Start();
      t2.Start();

      int result = t1.Result + t2.Result;
      textBox1.Text = result.ToString();
    }

    private int proc(object param)
    {
      System.Threading.Thread.Sleep(2000);
      List<int> inList = (List<int>)param;

      int result=0;
      foreach (int i in inList) {
        result += i;
      }

      return result;
    }

  }
}

解説

2つのTaskオブジェクトに渡すパラメータを準備します。今回は、List<int> 型のオブジェクトを用意します。それぞれのリストに2つずつ要素を追加します。
  int a = 9, b = 8, c = 7, d = 6;

  List<int> inparam1 = new List<int>();
  inparam1.Add(a);
  inparam1.Add(b);

  List<int> inparam2 = new List<int>();
  inparam2.Add(c);
  inparam2.Add(d);

Taskオブジェクトを作成します。コンストラクタの第二引数にはラムダ式を与えます。今回のラムダ式では、引数を1つ持ち、procメソッドの結果を返すラムダ式になります。2つのTaskオブジェクトにそれぞれ別のリストオブジェクトをパラメーターとして渡します。
  Task<int> t1 = new Task<int>((param) => proc(param), inparam1);
  Task<int> t2 = new Task<int>((param) => proc(param), inparam2);

Taskオブジェクトの開始は Start メソッドを呼び出します。
  t1.Start();
  t2.Start();

Taskオブジェクトからの結果を待ちます。結果は Taskオブジェクトの Result プロパティで取得できます。結果が戻るまでの間、 結果を待つため、このスレッドは停止します。スレッドが停止状態になるため、UIの操作などはできなくなります。
結果を取得後、2つのTaskオブジェクトの結果を合計してテキストボックスに表示します。
  int result = t1.Result + t2.Result;
  textBox1.Text = result.ToString();

実行結果

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


クリックから2秒ほど経過して、テキストボックスに結果が表示されます。パラメーターの数値がすべて合計された値が表示されます。
また、ボタンクリックから結果表示まではメインスレッドがブロックされるため、ウィンドウ操作ができなくなり画面が固まることも確認できます。

補足 : Task.Runメソッドを利用する場合

引数が無く、戻り値のみのFunc型の場合はRunメソッドで実行する記述ができますが、引数がある場合 Func<TResult, T> の型を取れるRunメソッドが無いため、 Runメソッドを利用した記述はできません。ラムダ式を利用する記述などに変える必要があります。
コンパイルエラーになるコード
  Task<int> t1 = Task<int>.Run<int>(proc, inparam1);
  Task<int> t2 = Task<int>.Run<int>(proc, inparam2);
著者
iPentec.com の代表。ハードウェア、サーバー投資、管理などを担当。
Office 365やデータベースの記事なども担当。
最終更新日: 2021-08-24
作成日: 2020-02-05
iPentec all rights reserverd.