[C#] 41. Taskクラスとasync、awaitを使い方


Study / C#    作成日付 : 2021/10/01 18:59:14   修正日付 : 2021/10/01 18:59:14

こんにちは。明月です。


この投稿はTaskクラスとasync、awaitを使い方に関する説明です。


以前の投稿でThreadに関して説明したことがあります。

link - [C#] 37. スレッド(Thread)を使い方、Thread.Sleep関数を使い方


スレッドとは並列処理だということを何度も説明したので、この投稿では省略します。

Threadを生成する時にシステムのリソースを使って逆にThreadが多すぎるならシステムの性能が落ちます。そのためスレッドプールに生成してスレッドの個数制限、スレッドのリソースを再活用してシステムの性能を改善することができます。

でも、スレッドプールはスレッドのステータスを制御することができないので、スレッドが終了する時まで待つ(Join)機能を実装しなければならない不便があります。

TaskはThreadPool中で動くスレッドだし、Threadみたいに簡単に生成してJoin機能まで使える機能があります。

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;

namespace Example
{
  // スレッドのパラメータクラス
  class Node
  {
    // コンソールが出力する時に使うテキスト
    public string Text { get; set; }
    // 繰り返しの回数
    public int Count { get; set; }
    // Sleepの時間チック
    public int Tick { get; set; }
  }
  class Program
  {
    // 実行関数
    static void Main(string[] args)
    {
      // ThreadPoolの最小スレッド個数は0個、最大スレッド個数は2個で設定
      ThreadPool.SetMinThreads(0, 0);
      ThreadPool.SetMaxThreads(2, 0);
      // リストにTaskを設定する。
      var list = new List<Task<int>>();
      // Taskに使えるラムダ式の入力値はobject、返却値はintタイプ
      var func = new Func<object, int>((x) =>
      {
        // objectをNodeタイプで強制キャスト
        var node = (Node)x;
        // 変数
        int sum = 0;
        // 設定された繰り返し回数ほど
        for (int i = 0; i <= node.Count; i++)
        {
          // 値を足す
          sum += i;
          // コンソールが出力
          Console.WriteLine(node.Text + " = " + i);
          // 設定されたSleep時間チック
          Thread.Sleep(node.Tick);
        }
        // コンソール出力
        Console.WriteLine("Completed " + node.Text);
        // 合計の値をリターン
        return sum;
      });
      // リストにTask追加
      list.Add(new Task<int>(func, new Node { Text = "A", Count = 5, Tick = 1000 }));
      list.Add(new Task<int>(func, new Node { Text = "B", Count = 5, Tick = 10 }));
      list.Add(new Task<int>(func, new Node { Text = "C", Count = 5, Tick = 500 }));
      list.Add(new Task<int>(func, new Node { Text = "D", Count = 5, Tick = 300 }));
      list.Add(new Task<int>(func, new Node { Text = "E", Count = 5, Tick = 200 }));
      // listに格納したTaskを実行
      list.ForEach(x => x.Start());
      // listに格納したTaskを終了するまで実行
      list.ForEach(x => x.Wait());
      // スレッドの合計を出力
      Console.WriteLine("Sum = " + list.Sum(x => x.Result));
      // 任意のキーを押してください
      Console.WriteLine("Press Any key...");
      Console.ReadLine();
    }
  }
}


上の結果をみれば先にThreadPoolで設定したスレッド制限設定がTaskで宣言したスレッドにも影響されることを確認できます。

つまり、実装はThreadみたいに簡単に使えますが、内容はThreadPoolで動くことを確認できます。


そしてThreadPoolと違い、return値を受け取ることができて1から5まで足すと15、スレッドが5個なので総合の75の結果が出ることを確認できます。

lockを使えなくても各スレッドで結果の値を受け取ってメインプロセスでスレッドの値を受け取って使えます。


そしてTaskの別の機能はasync、awaitキーワードと密接な関係があります。

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;

namespace Example
{
  // スレッドのパラメータクラス
  class Node
  {
    // コンソールが出力する時に使うテキスト
    public string Text { get; set; }
    // 繰り返しの回数
    public int Count { get; set; }
    // Sleepの時間チック
    public int Tick { get; set; }
  }
  class Program
  {
    // asyncが設定されたメソッド
    private static async Task<int> RunAsync(Node node)
    {
      // Taskに使えるラムダ式の入力値はobject、返却値はintタイプ
      var task = new Task<int>(() =>
      {
        // 変数
        int sum = 0;
        // 設定された繰り返し回数ほど
        for (int i = 0; i <= node.Count; i++)
        {
          // 値を足す
          sum += i;
          // コンソールが出力
          Console.WriteLine(node.Text + " = " + i);
          // 設定されたSleep時間チック
          Thread.Sleep(node.Tick);
        }
        // コンソール出力
        Console.WriteLine("Completed " + node.Text);
        // 合計の値をリターン
        return sum;
      });
      // Task実行
      task.Start();
      // taskが終了する時まで待機
      await task;
      // taskの結果リターン
      return task.Result;
    }
    // 実行関数
    static void Main(string[] args)
    {
      // ThreadPoolの最小スレッド個数は0個、最大スレッド個数は2個で設定
      ThreadPool.SetMinThreads(0, 0);
      ThreadPool.SetMaxThreads(2, 0);
      // リストにTaskを設定する。
      var list = new List<Task<int>>();
      // リストにTask追加
      list.Add(RunAsync(new Node { Text = "A", Count = 5, Tick = 1000 }));
      list.Add(RunAsync(new Node { Text = "B", Count = 5, Tick = 10 }));
      list.Add(RunAsync(new Node { Text = "C", Count = 5, Tick = 500 }));
      list.Add(RunAsync(new Node { Text = "D", Count = 5, Tick = 300 }));
      list.Add(RunAsync(new Node { Text = "E", Count = 5, Tick = 200 }));
      // スレッドの合計を出力
      Console.WriteLine("Sum = " + list.Sum(x => x.Result));
      // 任意のキーを押してください
      Console.WriteLine("Press Any key...");
      Console.ReadLine();
    }
  }
}


始めの例と結果は同じですが、Taskをもっと扱いやすく実装されています。

asyncが宣言された関数でTaskを生成して実行し、awaitでスレッドが終了するまで待機します。

そして結果をリターンするならMainプロセスで結果を合算して結果が出ることを確認できます。


ここまでTaskとasync、awaitの基本構造です。

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;

namespace Example
{
  // スレッドのパラメータクラス
  class Node
  {
    // コンソールが出力する時に使うテキスト
    public string Text { get; set; }
    // 繰り返しの回数
    public int Count { get; set; }
    // Sleepの時間チック
    public int Tick { get; set; }
  }
  class Program
  {
    // asyncが設定されたメソッド
    private static async Task<int> RunAsync(Node node)
    {
      // Taskに使えるラムダ式の入力値はobject、返却値はintタイプ
      var task = new Task<int>(() =>
      {
        // 変数
        int sum = 0;
        // 設定された繰り返し回数ほど
        for (int i = 0; i <= node.Count; i++)
        {
          // 値を足す
          sum += i;
          // コンソールが出力
          Console.WriteLine(node.Text + " = " + i);
          // 設定されたSleep時間チック
          Thread.Sleep(node.Tick);
        }
        // コンソール出力
        Console.WriteLine("Completed " + node.Text);
        // 合計の値をリターン
        return sum;
      });
      // Task実行
      task.Start();
      // taskが終了する時まで待機
      await task;
      // taskの結果リターン
      return task.Result;
    }
    // 実行関数
    static void Main(string[] args)
    {
      // ThreadPoolの最小スレッド個数は0個、最大スレッド個数は2個で設定
      ThreadPool.SetMinThreads(0, 0);
      ThreadPool.SetMaxThreads(2, 0);
      // リストにTaskを設定する。
      var list = new List<Task<int>>();

      // リストにTask追加
      // ContinueWith関数を使ってスレッドの結果を受け取ったら計算を追加する。
      list.Add(RunAsync(new Node { Text = "A", Count = 5, Tick = 1000 }).ContinueWith(x => x.Result * 100));
      list.Add(RunAsync(new Node { Text = "B", Count = 5, Tick = 10 }).ContinueWith(x => x.Result * 100));
      list.Add(RunAsync(new Node { Text = "C", Count = 5, Tick = 500 }).ContinueWith(x => x.Result * 100));
      list.Add(RunAsync(new Node { Text = "D", Count = 5, Tick = 300 }).ContinueWith(x => x.Result * 100));
      list.Add(RunAsync(new Node { Text = "E", Count = 5, Tick = 200 }).ContinueWith(x => x.Result * 100));
      // スレッドの合計を出力
      Console.WriteLine("Sum = " + list.Sum(x => x.Result));
      // 任意のキーを押してください
      Console.WriteLine("Press Any key...");
      Console.ReadLine();
    }
  }
}


TaskでContinueWithの関数を提供しますが、これは各スレッドが終了すれば続けて処理するラムダ処理です。

状況により他のTaskスレッドを付けることもできるし、様々な実行を連結して実行できる関数です。


Taskは.Net framework 4.0から追加された機能なので、もし以前のプレームワークならThreadPoolを使わなければならないです。

私も最近までThreadPoolをよく使いましたが、Taskスレッドの使い方に適応した後はTaskスレッドがすごく慣れて、ThreadやThreadPoolはもう使いませんね。

そしてasync、awaitのキーワードにより可読性もすごくよくなり、個人的に並列処理を作成するならTaskを利用して作成することをお勧めします。


ここまでTaskクラスとasync、awaitを使い方に関する説明でした。


ご不明なところや間違いところがあればコメントしてください。

#C#
最新投稿