非同期関数での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.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

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

    private async void button2_Click(object sender, EventArgs e)
    {
      await Task.Run(proc1);
      await Task.Run(proc2);
    }

    private void proc1()
    {
      System.Threading.Thread.Sleep(3000);//重い処理

      MessageBox.Show("proc1が完了しました。");
    }

    private void proc2()
    {
      System.Threading.Thread.Sleep(4000);//重い処理

      MessageBox.Show("proc2が完了しました。");
    }
  }
}

解説

こちらの記事で紹介した、Runメソッドを利用してTaskオブジェクトの処理を実行するコードを非同期化したコードになります。
button2_Click メソッドには await を追記して非同期関数にします。Task.Run メソッドを利用してタスクの処理を実行します。await を先頭に追記して処理を待つ動作にします。

また、Task.Run メソッドの引数にラムダ式を与える記述方法でも同じ動作になります。
  private async void button2_Click(object sender, EventArgs e)
  {
    await Task.Run(() => proc1());
    await Task.Run(() => proc2());

  }

実行結果

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

ボタンクリックから2秒経過すると "proc1が完了しました。" のメッセージボックスが表示されます。


メッセージボックスの[OK]ボタンをクリックし、さらに4秒経過すると、"proc2が完了しました。"のメッセージボックスが表示されます。


await を記述したことで、各処理が完了するまでメインスレッドが待機していることが確認できます。また、非同期メソッドのため、 待機中であってもメインウィンドウの移動やボタンのクリックなどの操作ができることも確認できます。

プログラム2 : Runメソッドを利用して、処理部のメソッドをインラインで記述する

先のプログラムは、クラスに実装されているメソッドを指定してTaskを実行していますが、1度限りの処理などの場合は、メソッドでの記述よりもインラインで記述したい場合があります。
ラムダ式を利用すると、処理部をインラインで記述できます。

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 FormAsyncTaskActionSimple : Form
  {
    public FormAsyncTaskActionSimple()
    {
      InitializeComponent();
    }

    private async void button3_Click(object sender, EventArgs e)
    {
      Func<Task> proc1_inl = async() =>
      {
        await Task.Delay(3000);//重い処理
        MessageBox.Show("proc1(インライン)が完了しました。");
      };

      Func<Task> proc2_inl = async () =>
      {
        await Task.Delay(4000);//重い処理
        MessageBox.Show("proc2(インライン)が完了しました。");
      };

      await Task.Run(proc1_inl);
      await Task.Run(proc2_inl);
    }
  }
}

解説

Task.Runメソッドの下記のコードは次のコードで置き換えることもできます。
  await Task.Run(proc1_inl);
  await Task.Run(proc2_inl);

一つめは、Runメソッドの引数にラムダ式を与える記述方法。
  await Task.Run(() => proc1());
  await Task.Run(() => proc1());

もう一つは、Runメソッドを使わずに デリゲートの呼び出しで、直接メソッドを呼び出す方法も可能です。
  await proc1_inl();
  await proc2_inl();

下記のラムダ式にasync を記述するコードでも動作しますが、ラムダ式の右辺にawaitが無いため、ワーニングになり、同期実行になります。(意味がないコードになってしまいます。)
  await Task.Run(async () => proc1());
  await Task.Run(async () => proc2());

また、デリゲートへの代入をせずに、直接Runメソッド内に実装部分のラムダ式を記述する書き方もできます。
いきなりこのコードを出されると、慣れていないと、わかりにくいですがRunメソッドで非同期関数を実行している処理になります。
    private async void button5_Click_1(object sender, EventArgs e)
    {
      await Task.Run(
        async () =>
        {
          await Task.Delay(3000);//重い処理
          MessageBox.Show("proc1(インライン)が完了しました。");
        }
        );

      await Task.Run(
       async () =>
       {
         await Task.Delay(4000);//重い処理
         MessageBox.Show("proc2(インライン)が完了しました。");
       }
      );
    }

また、下記のコードの記述での動作は可能ですが、
  Func<Task> proc1_inl = async () =>
  {
    await Task.Delay(3000);//重い処理
    MessageBox.Show("proc1(インライン)が完了しました。");
  };
  await proc1_inl();

以下のように直接ラムダ式を実行することはできないです。
コンパイルエラーになるコード
  await async () =>
  {
    await Task.Delay(3000);//重い処理
    MessageBox.Show("proc1(インライン)が完了しました。");
  }();
コンパイルエラーになるコード
  async () =>
  {
    await Task.Delay(3000);//重い処理
    MessageBox.Show("proc1(インライン)が完了しました。");
  };

プログラム3 : Runメソッドを利用せずに、非同期関数を実行する

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

UI

Windows Formアプリケーションを作成し、下図のUIを作成します。このプログラムでは [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 FormAsyncTaskActionSimple : Form
  {
    public FormAsyncTaskActionSimple()
    {
      InitializeComponent();
    }

    private async void button4_Click(object sender, EventArgs e)
    {
      await procA();
      await procB();
    }

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

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

    private async Task procB()
    {
      await Task.Delay(4000);//重い処理
      //System.Threading.Thread.Sleep(4000); //この記述でもOK - ただし同期呼び出しになる

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

解説

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

呼び出し元では await キーワードを追記して非同期メソッドを呼び出すことで、メソッドの完了まで待機できます。

実行結果

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


ボタンクリックから3秒経過すると、メッセージダイアログボックスが表示されます。 メッセージダイアログが表示されるまでの間、呼び出された側のスレッドはブロックされますが、メインスレッドはブロックせずに待機するため、 メインウィンドウは固まらずに操作できることが確認できます。


メッセージボックスの[OK]ボタンをクリックしてメッセージボックスを閉じます。メッセージボックスを閉じてから4秒計画すると、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 FormAsyncTaskActionSimple : Form
  {
    public FormAsyncTaskActionSimple()
    {
      InitializeComponent();
    }

    private async void button1_Click(object sender, EventArgs e)
    {
      Task t1 = new Task(proc1);
      await t1.Start();
      Task t2 = new Task(proc2);
      await t2.Start();
    }

    private void proc1()
    {
      System.Threading.Thread.Sleep(3000);//重い処理

      MessageBox.Show("proc1が完了しました。");
    }

    private void proc2()
    {
      System.Threading.Thread.Sleep(4000);//重い処理

      MessageBox.Show("proc2が完了しました。");
    }
  }
}

解説

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


awaitでvoid の関数を待機してしまうと戻り値が無いため、関数の終了が検出できないため、上記のエラーが発生します。
著者
iPentecのメインプログラマー
C#, ASP.NET の開発がメイン、少し前まではDelphiを愛用
最終更新日: 2020-05-20
作成日: 2020-05-08
iPentec all rights reserverd.