[C#] 61. ウィンドウフォーム(Window form)でスレッド(Thread)を使い方、クロススレッド問題解決


Study / C#    作成日付 : 2021/11/04 19:29:51   修正日付 : 2021/11/04 19:30:43

こんにちは。明月です。


この投稿はC#のウィンドウフォーム(Window form)でスレッド(Thread)を使い方、クロススレッド問題解決に関する説明です。


以前の投稿でC#のスレッドに関して説明したことがあります。

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


スレッドはプログラム内で並列処理することの意味です。まずウィンドウはシングルスレッドの無限ループで動いています。

でも、我々がボタンのクリックイベントでループを実行するロジックを作りましょう。

using System;
using System.Windows.Forms;
using System.Threading;

namespace WindowsFormsApp
{
  // Formクラスを継承
  public partial class Form1 : Form
  {
    // メンバー変数ボタン
    private Button button1 = null;
    // コントロールクラスの初期設定関数
    private T setInitControl<T>(T ctl, string name, int point) where T : Control
    {
      // 位置設定
      button.Location = new System.Drawing.Point(27, point);
      // コントロールの名設定
      button.Name = name;
      // コントロールのサイズ設定
      button.Size = new System.Drawing.Size(75, 23);
      // タブのIndex設定
      ctl.TabIndex = 0;
      // リターン
      return ctl;
    }
    // コンストラクタ
    public Form1()
    {
      // 初期化
      InitializeComponent();
      // ボタンインスタンス生成後、初期設定
      this.button1 = setInitControl(new Button(), "Button1", 40);
      // ボタンのText設定
      this.button1.Text = "Button";
      // フォームにControl追加
      this.Controls.Add(button1);
      // イベント追加(ラムダ式で追加)
      this.button1.Click += (sender, e) =>
      {
        // 0から9999までループ
        for (int i = 0; i < 10000; i++)
        {
          // コンソールに出力
          Console.WriteLine(i);
          // スレッド待機1秒
          Thread.Sleep(1000);
        }
      };
    }
  }
}


プログラムを実行してボタンを押下するとループが終わるまでウィンドウフォームは動きません。時間が流れたら応答なしになってプログラムが凍っている時もあります。

つまり、ウィンドウはシングルスレッド状況なのでその関数のスタックに掛けると処理が終わるまで動きません。


そうするとボタンを押下する時、複雑な処理をすると思えばどのように処理するでしょう?スレッドを利用すれば良いでしょう。

using System;
using System.Windows.Forms;
using System.Threading;

namespace WindowsFormsApp
{
  // Formクラスを継承
  public partial class Form1 : Form
  {
    // メンバー変数ボタン
    private Button button1 = null;
    // コントロールクラスの初期設定関数
    private T setInitControl<T>(T ctl, string name, int point) where T : Control
    {
      // 位置設定
      button.Location = new System.Drawing.Point(27, point);
      // コントロールの名設定
      button.Name = name;
      // コントロールのサイズ設定
      button.Size = new System.Drawing.Size(75, 23);
      // タブのIndex設定
      ctl.TabIndex = 0;
      // リターン
      return ctl;
    }
    // コンストラクタ
    public Form1()
    {
      // 初期化
      InitializeComponent();
      // ボタンインスタンス生成後、初期設定
      this.button1 = setInitControl(new Button(), "Button1", 40);
      // ボタンのText設定
      this.button1.Text = "Button";
      // フォームにControl追加
      this.Controls.Add(button1);
      // イベント追加(ラムダ式で追加)
      this.button1.Click += (sender, e) =>
      {
        // スレッドプール生成
        ThreadPool.QueueUserWorkItem((_) =>
        {
          // 0から9999までループ
          for (int i = 0; i < 10000; i++)
          {
            // コンソールに出力
            Console.WriteLine(i);
            // スレッド待機1秒
            Thread.Sleep(1000);
          }
        });
      };
    }
  }
}


ボタンをクリックしてコンソールに1ずつに出力してもウィンドウが凍らないことを確認できます。

そうすると今回はコンソールに出力することではなく、ウィンドウのコントロールで出力するように作成しましょう。

using System;
using System.Windows.Forms;
using System.Threading;

namespace WindowsFormsApp
{
  // Formクラスを継承
  public partial class Form1 : Form
  {
    // メンバー変数ボタン
    private Button button1 = null;
    private Label label1 = null;
    // コントロールクラスの初期設定関数
    private T setInitControl<T>(T ctl, string name, int point) where T : Control
    {
      // 位置設定
      button.Location = new System.Drawing.Point(27, point);
      // コントロールの名設定
      button.Name = name;
      // コントロールのサイズ設定
      button.Size = new System.Drawing.Size(75, 23);
      // タブのIndex設定
      ctl.TabIndex = 0;
      // リターン
      return ctl;
    }
    // コンストラクタ
    public Form1()
    {
      // 初期化
      InitializeComponent();
      // ボタンインスタンス生成後、初期設定
      this.button1 = setInitControl(new Button(), "Button1", 80);
      // ラベルインスタンス生成後、初期設定
      this.label1 = setInitControl(new Label(), "Label1", 40);
      // ボタンのText設定
      this.button1.Text = "Button";
      // フォームにControl追加
      this.Controls.Add(button1);
      this.Controls.Add(label1);
      // イベント追加(ラムダ式で追加)
      this.button1.Click += (sender, e) =>
      {
        // スレッドプール生成
        ThreadPool.QueueUserWorkItem((_) =>
        {
          // 0から9999までループ
          for (int i = 0; i < 10000; i++)
          {
            // Labelのテキスト設定
            this.label1.Text = $"{i}";
            // スレッド待機1秒
            Thread.Sleep(1000);
          }
        });
      };
    }
  }
}


ボタンをクリックするとすぐエラーが発生します。

理由はWindowで動いているスレッドとスレッドプールで動いているスレッドが同期化されてないからです。

スレッド間に同期化しようと思えば、お互いにlockを設定して同期化すればよいのに、ウィンドウメッセージを動いているスレッドにlockを掛ける方法がありません。

これをC#ウィンドウ開発ではクロススレッド問題と言います。


これを解決する方法が各コントロールにあるInvoke関数を利用してvisitorパターン、つまりコールバック関数という方法で処理ができます。

using System;
using System.Windows.Forms;
using System.Threading;

namespace WindowsFormsApp
{
  // 拡張関数ためのクラス
  public static class Util
  {
    // 拡張関数
    public static void InvokeControl(this Control ctl, Action func)
    {
      // Thread ID比較
      if (ctl.InvokeRequired)
      {
        // デリゲート関数で実行
        ctl.Invoke(func);
      }
      else
      {
        // Thread IDが同じならそのまま実行(同じスレッドという意味)
        func();
      }
    }
  }
  // Formクラスを継承
  public partial class Form1 : Form
  {
    // メンバー変数ボタン
    private Button button1 = null;
    private Label label1 = null;
    // コントロールクラスの初期設定関数
    private T setInitControl<T>(T ctl, string name, int point) where T : Control
    {
      // 位置設定
      button.Location = new System.Drawing.Point(27, point);
      // コントロールの名設定
      button.Name = name;
      // コントロールのサイズ設定
      button.Size = new System.Drawing.Size(75, 23);
      // タブのIndex設定
      ctl.TabIndex = 0;
      // リターン
      return ctl;
    }
    // コンストラクタ
    public Form1()
    {
      // 初期化
      InitializeComponent();
      // ボタンインスタンス生成後、初期設定
      this.button1 = setInitControl(new Button(), "Button1", 80);
      // ラベルインスタンス生成後、初期設定
      this.label1 = setInitControl(new Label(), "Label1", 40);
      // ボタンのText設定
      this.button1.Text = "Button";
      // フォームにControl追加
      this.Controls.Add(button1);
      this.Controls.Add(label1);
      // イベント追加(ラムダ式で追加)
      this.button1.Click += (sender, e) =>
      {
        // スレッドプール生成
        ThreadPool.QueueUserWorkItem((_) =>
        {
          // 0から9999までループ
          for (int i = 0; i < 10000; i++)
          {
            // デリゲート関数を通ってウィンドウスレッドから下記の関数を呼び出すことにする。
            this.label1.InvokeControl(() =>
            {
              // Labelのテキスト設定
              this.label1.Text = $"{i}";
            });
            // スレッド待機1秒
            Thread.Sleep(1000);
          }
        });
      };
    }
  }
}


ソース上にstatic Utilクラスを作成してControlクラスの拡張関数を作成しました。そしてスレッドプールの中でlabel1インスタンスにInvokeControl関数を呼び出してラムダ式でコールバック関数を作成しました。

ボタンをクリックするとLabelに数字が1秒単位で更新することを確認できます。


ここまでC#のウィンドウフォーム(Window form)でスレッド(Thread)を使い方、クロススレッド問題解決に関する説明でした。


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

最新投稿