非同期関数でのパラメーターと戻り値のある Task の利用 - C#

パラメーターがあり、戻り値がある場合の非同期関数を利用したTaskの利用について紹介します。

概要

こちらの記事では、Taskを利用して別スレッドで処理を実行する例を紹介しました。この記事では、Taskで処理するメソッドを非同期関数にするコードを紹介します。

プログラム1 : Task.Run メソッドを利用する

void関数が待機できない対策として、Runメソッドを利用する方法があります。
Windows Formアプリケーションを作成します。

UI

下図のUIを作成します。今回のプログラムでは [button2]のみを利用します。

コード

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

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

    private async void button2_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);

      int p1_result = await Task.Run<int>(() => proc(inparam1));
      textBox1.Text = p1_result.ToString();

      int p2_result = await Task.Run<int>(() => proc(inparam2));
      textBox1.Text = p1_result.ToString() + ", " + p2_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;
    }
  }
}

解説

Runメソッドのラムダ式には引数のないデリゲートを与えますが、ラムダ式の右辺での処理メソッド呼び出し時に、呼び出し元のパラメーターの変数を与えることで、
Taskオブジェクトにパラメーターを渡しています。~
  List<int> inparam1 = new List<int>();
  inparam1.Add(a);
  inparam1.Add(b);

  int p1_result = await Task.Run<int>(() => proc(inparam1));

実行結果

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


[button2]をクリックします。2秒ほどすると画面に変数abの和がテキストボックスに表示されます。表示されるまでの間、 メインスレッドはブロックされないため、ウィンドウの操作などはできることが確認できます。


さらに2秒ほど経過すると、変数cdの和がテキストボックスに表示されます。結果が表示されるまでの間 メインスレッドはブロックされないので、ウィンドウの操作などでき、UIはフリーズしません。

プログラム2 : Task.Factory.StartNew を利用する

Task.Run メソッドは戻り値が void 型のためawaitで待機できませんが、Task.Factory.StartNew メソッドを利用すると戻り値を受け取れるため、awaitで待機できます。

UI

下図のUIを作成します。今回のプログラムでは [button3]のみを利用します。

コード

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

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

    private async 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);

      int p1_result = await Task.Factory.StartNew(proc, inparam1);
      textBox1.Text = p1_result.ToString();

      int p2_result = await Task.Factory.StartNew(proc, inparam2);
      textBox1.Text = p1_result.ToString() + ", " + p2_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.Factory.StartNew() メソッドを呼び出して処理を実行します。第一引数に実行する処理のメソッド(デリゲート)を与えます。第二引数にパラメーターを与えます。
戻り値は Task<int>型ですが、awaitを記述して呼び出す場合は戻り値はintで受け取れます。
  int p1_result = await Task.Factory.StartNew(proc, inparam1);

実行結果

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


[button3]をクリックします。2秒ほどすると画面に変数abの和がテキストボックスに表示されます。表示されるまでの間、 メインスレッドはブロックされないため、ウィンドウの操作などはできることが確認できます。


さらに2秒ほど経過すると、変数cdの和がテキストボックスに表示されます。結果が表示されるまでの間 メインスレッドはブロックされないので、ウィンドウの操作などでき、UIはフリーズしません。

プログラム3 : Task.Run を利用しないコード

Task.Runを利用しないで非同期関数を実行するコードを紹介します。
戻り値のない void 関数は待機できませんので、Runメソッドを利用せずに非同期関数を実行して待機する場合には、単純に非同期関数を呼び出す処理になります。
この場合は戻り値がvoid の関数ではなく Task 型の戻り値を返す関数を記述して利用します。

UI

下図のUIを作成します。今回のプログラムでは [button4]のみを利用します。

コード

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

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

    private async void button4_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);

      int p1_result = await proc_a(inparam1);
      textBox1.Text = p1_result.ToString();

      int p2_result = await proc_a(inparam2);
      textBox1.Text = p1_result.ToString() + ", " + p2_result.ToString();
    }

    private async Task<int> proc_a(List<int> param)
    {
      await Task.Delay(3000);

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

      return result;
    }
  }
}

解説

非同期関数として実装する場合、戻り値の型をTask<戻り値の型> とします。元の関数がint型の戻り値の関数でしたので、 戻り値の型をTask<int>とします。
  private async Task<int> proc_a(List<int> param)
  {
    await Task.Delay(3000);

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

非同期関数の呼び出しは、awaitを付けて関数を呼び出す記述です。非同期関数の戻り値はTask<int>ですが、awaitを付けて呼び出した場合の戻り値はintで受け取れます。
  int p1_result = await proc_a(inparam1);
  textBox1.Text = p1_result.ToString();

  int p2_result = await proc_a(inparam2);
  textBox1.Text = p1_result.ToString() + ", " + p2_result.ToString();

実行結果

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


[button4]をクリックします。2秒ほどすると、テキストボックスに変数abの和が表示されます。表示されるまでの間メインウィンドウがフリーズしないことが確認できます。


テキストボックスに表示されてから、さらに2秒ほど経過すると、テキストボックスに変数cd の和が表示されます。表示されるまでの間、メインウィンドウはフリーズしません。

エラーになる例 : 単純に async / await を追加した場合

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

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

    private async 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);

      await task1.Start();
      await 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;
    }
  }
}

解説

非同期での呼び出しにするため、呼び出し元のbutton1_Clickメソッドに asyncを追記しています。また、 タスクの開始時のStart() メソッドの呼び出し時に await を追記しています。 こちらのコードですが、コンパイルすると下記のコンパイルエラーが発生します。
エラー CS4008 void' を待機することができません
Task.Start() メソッドは戻り値のない void のため上記のエラーが発生します。
著者
iPentecのメインプログラマー
C#, ASP.NET の開発がメイン、少し前まではDelphiを愛用
最終更新日: 2021-08-24
作成日: 2020-02-07
iPentec all rights reserverd.