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

非同期関数でパラメーターはあるが戻り値が無い Task を利用するコードを紹介します。

概要

こちらの記事では、Taskを利用して並列に実行する方法を紹介しました。 この記事では、非同期関数を利用してTaskを実行して、処理が終了までメインスレッドをブロックせずに待機するコードを紹介します。

プログラム1 : Task.Run メソッドを利用し、ラムダ式を与える

上記のコードがエラーになるため、エラーにならないコードにするには、Runメソッドにラムダ式を与える記述にします。

UI

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

コード

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

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

    private async void button3_Click(object sender, EventArgs e)
    {
      string str1 = "ぺんぎん";
      string str2 = "あひる";

      await Task.Run(() => proc(str1));
      await Task.Run(() => proc(str2));
    }


    private void proc(object param)
    {
      System.Threading.Thread.Sleep(3000);
      string parameter_string = "";

      if (param != null) {
        parameter_string = (string)param;
      }

      MessageBox.Show("procが完了しました。" + parameter_string);
    }
  }
}

解説

Runメソッドのラムダ式には引数のないデリゲートを与えますが、ラムダ式の右辺での処理メソッド呼び出し時に、呼び出し元のローカル変数を与えることで、 Taskオブジェクトにパラメーターを渡しています。古典的なコードになれていると、トリッキーさを感じてしまいますが、この記述でパラメーターを渡す方法が正しいようです。
  string str1 = "ぺんぎん";
  string str2 = "あひる";

  await Task.Run(() => proc(str1));
  await Task.Run(() => proc(str2));

実行結果

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


[button3]をクリックします。ボタンクリックから3秒経過するとメッセージボックスが表示されます。 メッセージボックスのメッセージに呼び出し元から渡されたパラメータの値が表示されていることが確認できます。


メッセージボックスの[OK]ボタンをクリックします。メッセージボックスが閉じ、さらに3秒経過すると、メッセージボックスが 再度表示されます。メッセージボックス内のメッセージが先ほどと違うパラメータの値になっていることが確認できます。


また、メッセージボックスが表示されるまでの間、メインウィンドウは固まらずに操作できることも確認できます。

プログラム2 : Task.Run メソッドを利用し、実行メソッドをインラインで記述する

先のコードで希望する動作は実装できましたが、Taskでの処理内容をメソッドではなくラムダ式で宣言してインラインで記述することもできます。

UI

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

コード

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

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

    private async void button4_Click(object sender, EventArgs e)
    {
      string param = "ぺんぎん";

      Func<Task> proc_inl = async () =>
      {
        await Task.Delay(3000);
        //ystem.Threading.Thread.Sleep(3000);//この記述でもOK - ただし同期呼び出しになる

        string parameter_string = "";

        if (param != null) {
          parameter_string = param;
        }

        MessageBox.Show("proc_inlが完了しました。" + parameter_string);
      };

      await Task.Run(proc_inl);

    }
  }
}

解説

メソッドで記述されていた proc メソッドを呼び出し元のメソッド内にラムダ式を利用してインラインで記述しています。
パラメータの渡しは、処理の記述内に、呼び出し元のローカル変数を記述することでパラメーターを渡しています。
  string param = "ぺんぎん";

  Func<Task> proc_inl = async () =>
  {
    await Task.Delay(3000);
    //ystem.Threading.Thread.Sleep(3000);//この記述でもOK - ただし同期呼び出しになる

    string parameter_string = "";

    if (param != null) {
      parameter_string = param;
    }

    MessageBox.Show("proc_inlが完了しました。" + parameter_string);
  };

処理の実行開始はRunメソッドを利用します。引数にデリゲートを与えます。
  await Task.Run(proc_inl);

このコードはインラインで記述しているため、パラメーターが異なる処理を何度も実行するのには向いていないコードです。

実行結果

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


[button4]をクリックします。ボタンのクリックから3秒ほど経過すると、「proc_inlが完了しました。」のメッセージボックスが表示されます。
ボタンのクリックからダイアログが表示されるまでの間UIスレッドは待機状態になっていますが、スレッドはブロックされないため、メインウィンドウが固まらずに、 ウィンドウの移動や他のボタンのクリックなどの操作ができます。

プログラム3 : Task.Factory.StartNewメソッドを利用する

Task.Run() メソッドを利用した場合、パラメーターの与え方がラムダ式を利用したものになってしまいます。別の方法として、Task.Factory.StartNew()メソッドを利用すると、 第一引数に処理のデリゲートを与え、第二引数にパラメーターを与えることができ、Taskのコンストラクタでパラメーターを渡す方法と同様の記述が利用できます。
(TaskのコンストラクタでTaskオブジェクトを作成した場合、Startメソッドでの開始となってしまうため、awaitでの大気ができなくなってしまいます。)

UI

下図のフォームを作成します。今回のコードでは、[button6]のみを利用します。

コード

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

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

    private async void button6_Click(object sender, EventArgs e)
    {
      string str1 = "ぺんぎん";
      string str2 = "あひる";

      await Task.Factory.StartNew(proc, (object)str1);
      await Task.Factory.StartNew(proc, (object)str2);
    }

    private void proc(object param)
    {
      System.Threading.Thread.Sleep(3000);
      string parameter_string = "";

      if (param != null) {
        parameter_string = (string)param;
      }

      MessageBox.Show("procが完了しました。" + parameter_string);
    }
  }
}

解説

Task.Factory.StartNew()メソッドでタスクを作成して事項するコードが以下です。 第一引数にTaskオブジェクトで処理するデリゲート、第二引数にデリゲートに渡すパラメーターを与えます。
StartNewメソッドが呼び出されると同時に処理は開始します。StartNewメソッドはTask または Task<TResult> を戻り値にとるためawaitで待機できます。
  private async void button6_Click(object sender, EventArgs e)
  {
    string str1 = "ぺんぎん";
    string str2 = "あひる";

    await Task.Factory.StartNew(proc, (object)str1);
    await Task.Factory.StartNew(proc, (object)str2);
  }

Taskオブジェクトでの処理内容は先のプログラムと同様で、3秒待機した後、メッセージボックスを表示し「procが完了しました。」の文字列と 渡されたパラメーターの文字列を表示します。
  private void proc(object param)
  {
    System.Threading.Thread.Sleep(3000);
    string parameter_string = "";

    if (param != null) {
      parameter_string = (string)param;
    }

    MessageBox.Show("procが完了しました。" + parameter_string);
  }

実行結果

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


[button6]をクリックします。3秒ほど経過すると、下図のメッセージボックスが表示されます。Taskオブジェクトに渡されたパラメーターの文字列が メッセージボックスに表示されていることが確認できます。また、メッセージボックスが表示されるまでの間、メインウィンドウはロックされず ウィンドウの操作ができることも確認できます。


メッセージボックスの[OK]ボタンをクリックします。メッセージボックスが閉じられてから3秒ほど計画すると次のメッセージボックスが表示されます。 先ほどのメッセージボックスとは異なるパラメーターの文字列が表示されることが確認できます。 メッセージボックスが閉じられてから次の処理が開始されることが確認でき、await 呼び出しで待機できていることもわかります。
また、メッセージボックスが表示されるまでの間、メインウィンドウはロックされず ウィンドウの操作ができることも確認できます。

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

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

UI

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

コード

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

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

    private async void button5_Click(object sender, EventArgs e)
    {
      string str1 = "ぺんぎん";
      string str2 = "あひる";

      await proc(str1);
      await proc(str2);
    }

    private async Task proc(string param)
    {
      await Task.Delay(3000);
      //System.Threading.Thread.Sleep(3000);//この記述でもOK - ただし同期呼び出しになる

      string parameter_string = "";

      if (param != null) {
        parameter_string = (string)param;
      }

      MessageBox.Show("proc(非同期)が完了しました。" + parameter_string);
    }
  }
}

解説

voidのメソッド(関数)は待機できないため、待機できる非同期関数を作成するには、メソッドに async キーワードを追記するとともに、Task型の戻り値を返す関数に変更します。
この記述で待機可能な非同期関数が実装できます。

呼び出し元では await キーワードを追記して非同期メソッドを呼び出すことで、メソッドの完了まで待機できます。
パラメーターの与え方は、通常の関数と同様に関数名(メソッド名)の後ろの「()」内にパラメータを記述します。

動作結果

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


[button5]をクリックします。ボタンのクリックから3秒ほど経過すると、「proc(非同期)が完了しました。」のメッセージとパラメーターとして与えた文字列(str1の変数値)のメッセージボックスが表示されます。
ボタンのクリックからダイアログが表示されるまでの間UIスレッドは待機状態になっていますが、スレッドはブロックされないため、メインウィンドウが固まらずに、 ウィンドウの移動や他のボタンのクリックなどの操作ができます。


メッセージボックスの[OK]ボタンをクリックします。さらに3秒ほど経過すると、2つ目のダイアログボックスが表示されます。「proc(非同期)が完了しました。」のメッセージと、 パラメーターとして与えた文字列(str2の変数値)のメッセージボックスが表示されます。
2つ目のメッセージボックスが表示されるまでの間もメインウィンドウの操作は固まらずに、ウィンドウの移動などできることが確認できます。

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

はじめにエラーになる例を紹介します。

コード

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

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

    private async void button1_Click(object sender, EventArgs e)
    {
      string str1 = "ぺんぎん";
      string str2 = "あひる";

      Task task1 = new Task(proc, (object)str1);
      Task task2 = new Task(proc, (object)str2);

      await task1.Start();
      await task2.Start();
    }

    private void proc(object param)
    {
      System.Threading.Thread.Sleep(3000);
      string parameter_string = "";

      if (param != null) {
        parameter_string = (string)param;
      }

      MessageBox.Show("procが完了しました。" + parameter_string);
    }
  }
}

解説

非同期での呼び出しにするため、呼び出し元のbutton1_Clickメソッドに asyncを追記しています。また、 タスクの開始時のStart() メソッドの呼び出し時に await を追記しています。
こちらのコードですが、コンパイルすると下記のコンパイルエラーが発生します。
エラー CS4008 void' を待機することができません
awaitでvoid の関数を待機してしまうと戻り値が無いため、関数の終了が検出できないため、上記のエラーが発生します。

エラーになる例(その2) : Task.Runメソッドにメソッドを与える場合

パラメーターが無い場合には、Task.Runメソッドにメソッドを与える記述ができますが、パラメーターがある場合にはエラーになります。

下記のコードの場合は、次のエラーが発生します。
  private async void button2_Click(object sender, EventArgs e)
  {
    await Task.Run(proc);
  }
エラー CS1503 引数 1: は 'メソッド グループ' から 'Action' へ変換することはできません。

また、下記のコードの場合は、次のエラーが発生します。
  private async void button2_Click(object sender, EventArgs e)
  {
    string str1 = "ぺんぎん";
    string str2 = "あひる";

    await Task.Run(proc(str1));
    await Task.Run(proc(str2));
  }
エラー CS1503 引数 1: は 'void' から 'System.Action' へ変換することはできません。

どちらの場合でも、Task.Runメソッドには引数のないActionを与える必要があるため、Action<T> を与えることはできません。
著者
iPentecのメインプログラマー
C#, ASP.NET の開発がメイン、少し前まではDelphiを愛用
最終更新日: 2021-08-24
作成日: 2020-02-07
iPentec all rights reserverd.