[C#] 38. ThreadPoolの使い方


Study / C#    作成日付 : 2019/07/23 00:05:40   修正日付 : 2021/09/27 19:33:26

こんにちは。明月です。


この投稿はC#のThreadPoolの使い方に関する説明です。


以前の投稿でThreadに関して説明しました。

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


Threadとはプログラム内で並列処理のための機能です。

でも、このThreadは個数を制御することができないと逆にプログラムの性能が落ちるということも説明しました。そのため、Thread個数を管理することも作成しなければならないですが、.Net frameworkにはこのThreadの個数を管理する機能があり、それがThreadPoolです。

using System;
using System.Threading;

namespace Example
{
  // スレッドのパラメータクラス
  class Node
  {
    // コンソールが出力する時に使うテキスト
    public string Text { get; set; }
    // 繰り返しの回数
    public int Count { get; set; }
    // Sleepの時間チック
    public int Tick { get; set; }
  }
  class Program
  {
    // スレッド実行関数
    static void ThreadProc(Object callBack)
    {
      // パラメータ値がNodeクラスタイプではなければ終了
      if (callBack.GetType() != typeof(Node))
      {
        return;
      }
      // Nodeタイプで強制キャスト(データタイプがObjectタイプ)
      var node = (Node)callBack;
      // 設定された繰り返し回数ほど
      for (int i = 0; i < node.Count; i++)
      {
        // コンソールが出力
        Console.WriteLine(node.Text + " = " + i);
        // 設定されたSleep時間チック
        Thread.Sleep(node.Tick);
      }
      // 完了、コンソールが出力
      Console.WriteLine("Complete " + node.Text);
    }
    // 実行関数
    static void Main(string[] args)
    {
      // ThreadPoolの最小スレッド個数は0個、最大スレッド個数は2個で設定
      // 参考、二つ目のパラメータは非同期I/Oスレッド個数(ただ、0で設定しても関係ない。)
      if (ThreadPool.SetMinThreads(0, 0) && ThreadPool.SetMaxThreads(2, 0))
      {
        // ThreadPoolに登録、デリゲート関数でThreadProcを登録
        // パラメータでNodeインスタンスを生成して渡す
        ThreadPool.QueueUserWorkItem(ThreadProc, new Node { Text = "A", Count = 3, Tick = 1000 });
        ThreadPool.QueueUserWorkItem(ThreadProc, new Node { Text = "B", Count = 5, Tick = 10 });
        ThreadPool.QueueUserWorkItem(ThreadProc, new Node { Text = "C", Count = 2, Tick = 500 });
        ThreadPool.QueueUserWorkItem(ThreadProc, new Node { Text = "D", Count = 7, Tick = 300 });
        ThreadPool.QueueUserWorkItem(ThreadProc, new Node { Text = "E", Count = 4, Tick = 200 });
      }
      // 任意のキーを押してください
      Console.WriteLine("Press Any key...");
      Console.ReadLine();
    }
  }
}


上の例をみれば私がThreadPoolクラスを利用してスレッドを総5個生成します。

でも、ThreadPoolには同時に実行するスレッド個数は0個から最大に2個まで生成されるように設定しました。つまり、Threadを無制限に生成して実行することではなく、Pool中で総量を設定してその以上は待機状況に待ちます。

つまり、結果を見ても、AとBは同時に実行されますが、CからはBが終了された状況で実行されることを確認でいます。Threadを最大に2個を実行しますが、その以上は実行されない設定でThreadPoolを管理します。


ThreadPoolにはThreadの生成を管理することになります。

でも、このThreadPoolはJoin関数がないので、Threadのすべてが終了する時まで待つ関数がありません。

using System;
using System.Threading;

namespace Example
{
  // スレッドのパラメータクラス
  class Node
  {
    // コンソールが出力する時に使うテキスト
    public string Text { get; set; }
    // 繰り返しの回数
    public int Count { get; set; }
    // Sleepの時間チック
    public int Tick { get; set; }
  }
  class Program
  {
    // スレッド実行関数
    static void ThreadProc(Object callBack)
    {
      // パラメータ値がNodeクラスタイプではなければ終了
      if (callBack.GetType() != typeof(Node))
      {
        return;
      }
      // Nodeタイプで強制キャスト(データタイプがObjectタイプ)
      var node = (Node)callBack;
      // 設定された繰り返し回数ほど
      for (int i = 0; i < node.Count; i++)
      {
        // コンソールが出力
        Console.WriteLine(node.Text + " = " + i);
        // 設定されたSleep時間チック
        Thread.Sleep(node.Tick);
      }
      // 完了、コンソールが出力
      Console.WriteLine("Complete " + node.Text);
    }
    // スレッドプールのJoin設定関数
    static void ThreadPoolJoin(int size)
    {
      // 無限ループ
      while (true)
      {
        // 1秒待機
        Thread.Sleep(1000);
        // 現在待機中のスレッド個数を取得するための変数
        int count = 0;
        int iocount = 0;
        // 現在、使用可能なあまりスレッド個数を取得
        ThreadPool.GetAvailableThreads(out count, out iocount);
        // Max Threadと同じなら無限ループ終了
        if (count == size)
        {
          // 無限ループ終了
          break;
        }
      }
    }
    // 実行関数
    static void Main(string[] args)
    {
      // ThreadPoolの最小スレッド個数は0個、最大スレッド個数は2個で設定
      // 参考、二つ目のパラメータは非同期I/Oスレッド個数(ただ、0で設定しても関係ない。)
      if (ThreadPool.SetMinThreads(0, 0) && ThreadPool.SetMaxThreads(2, 0))
      {
        // ThreadPoolに登録、デリゲート関数でThreadProcを登録
        // パラメータでNodeインスタンスを生成して渡す
        ThreadPool.QueueUserWorkItem(ThreadProc, new Node { Text = "A", Count = 3, Tick = 100 });
        ThreadPool.QueueUserWorkItem(ThreadProc, new Node { Text = "B", Count = 5, Tick = 10 });
        ThreadPool.QueueUserWorkItem(ThreadProc, new Node { Text = "C", Count = 2, Tick = 500 });
        ThreadPool.QueueUserWorkItem(ThreadProc, new Node { Text = "D", Count = 7, Tick = 300 });
        ThreadPool.QueueUserWorkItem(ThreadProc, new Node { Text = "E", Count = 4, Tick = 200 });
      }
      // 総2個のスレッドが待機状況になるまで待つ
      ThreadPoolJoin(2);
      // 任意のキーを押してください
      Console.WriteLine("Press Any key...");
      Console.ReadLine();
    }
  }
}


上の例はThreadPoolJoinという関数を作成してThreadPoolで使えるスレッド個数を確認してスレッドを使える最大量と同じなら無限ループが終了することでJoin関数を作成しました。

簡単なプログラムなら上みたいにThreadPoolを制御することができます。


問題は大きいプログラムならThreadPoolクラスの特性がstaticなのでどのところで使っているか確認することが難しいです。

ですから設計しているところでThreadPoolの使いが終了しました。と思っても、ライブラリや他のクラスでThreadPoolを使うと思えば、待機状況の個数でJoinをコントロールすると予想できないバグが発生する可能性があります。


それでTaskで使えるEventWaitHandleを利用すれば部分的にThreadPoolを制御することができます。

using System;
using System.Threading;
using System.Collections.Generic;

namespace Example
{
  // スレッドのパラメータクラス
  // EventWaitHandleクラスの継承
  public class Node : EventWaitHandle
  {
    // コンストラクタ設定
    public Node() : base(false, EventResetMode.ManualReset) { }
    // コンソールが出力する時に使うテキスト
    public string Text { get; set; }
    // 繰り返しの回数
    public int Count { get; set; }
    // Sleepの時間チック
    public int Tick { get; set; }
  }
  class Program
  {
    // スレッド実行関数
    static void ThreadProc(Object callBack)
    {
      // パラメータ値がNodeクラスタイプではなければ終了
      if (callBack.GetType() != typeof(Node))
      {
        // EventWaitHandleのJoin設定終了
        node.Set();
        return;
      }
      // Nodeタイプで強制キャスト(データタイプがObjectタイプ)
      var node = (Node)callBack;
      // 設定された繰り返し回数ほど
      for (int i = 0; i < node.Count; i++)
      {
        // コンソールが出力
        Console.WriteLine(node.Text + " = " + i);
        // 設定されたSleep時間チック
        Thread.Sleep(node.Tick);
      }
      // 完了、コンソールが出力
      Console.WriteLine("Complete " + node.Text);
      // EventWaitHandleのJoin設定終了
      node.Set();
    }
    // Nodeクラスのインスタンスを追加した後、Listに登録
    static EventWaitHandle AddNode(List<EventWaitHandle> list, string text, int count, int tick)
    {
      // インスタンス生成
      var node = new Node { Text = text, Count = count, Tick = tick };
      // listに登録
      list.Add(node);
      // インスタンスをリターン
      return node;
    }
    // 実行関数
    static void Main(string[] args)
    {
      // JoinのためのEventWaitHandleリスト
      var list = new List<EventWaitHandle>();
      // ThreadPoolの最小スレッド個数は0個、最大スレッド個数は2個で設定
      // 参考、二つ目のパラメータは非同期I/Oスレッド個数(ただ、0で設定しても関係ない。)
      if (ThreadPool.SetMinThreads(0, 0) && ThreadPool.SetMaxThreads(2, 0))
      {
        // ThreadPoolに登録、デリゲート関数でThreadProcを登録
        // 関数AddNodeを呼び出してインスタンスを生成して上のlistに登録
        ThreadPool.QueueUserWorkItem(ThreadProc, AddNode(list, "A", 3, 1000));
        ThreadPool.QueueUserWorkItem(ThreadProc, AddNode(list, "B", 5, 10));
        ThreadPool.QueueUserWorkItem(ThreadProc, AddNode(list, "C", 2, 500));
        ThreadPool.QueueUserWorkItem(ThreadProc, AddNode(list, "D", 7, 300));
        ThreadPool.QueueUserWorkItem(ThreadProc, AddNode(list, "E", 4, 200));
      }
      // listにあるEventWaitHandle関数がすべてSetが呼び出したらJoin解除
      WaitHandle.WaitAll(list.ToArray());

      // 任意のキーを押してください
      Console.WriteLine("Press Any key...");
      Console.ReadLine();
    }
  }
}


listをローカル変数に設定してWaitHandle.WaitAllから並列に変換すればThreadPoolが終了する時まで制御ができます。

ThreadPoolが全体に使っているスレッド個数を制御することですごく便利ですが、生成されたスレッドを待つ機能(Join)に関してはずいぶん大変ですね。

でも、性能のためにThreadをそのままに使うことよりThreadPoolを利用することがシステム性能のためにもっといい機能です。


参考でSetMinThreads関数とSetMaxThreads関数でスレッド個数の制御を設定しても、Threadで生成されるスレッドには影響がありません。つまり、Poolの設定を2個だけ設定してもThreadインスタンスは10個を設定することが可能です。


ここまでC#のThreadPoolの使い方に関する説明でした。


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

#C#
最新投稿