C#

C# サブスレッドからフォームの表示を行う(Invoke)

C#でInvokeを使うメリットなど

C#のInvokeメソッドは、別のスレッドで実行する必要があるコードを、UIスレッド(通常はメインスレッド)から実行するときなど使用します。

たとえば、クロススレッドのUI更新: UIスレッド以外のスレッドからUIを更新しようとすると、クロススレッドの例外が発生することがあります。そのような時にInvokeメソッドを使用すると、UIスレッドでコードを実行し、UIの更新を正確かつ安全に行うことができるようになります。

また、 長時間実行される処理をUIスレッドで実行すると、UIがフリーズしてしまい、ユーザーインタフェースが応答するレスポンシブでなくなる可能性があります。そんな場合に、Invokeメソッドを使用すると、時間のかかる処理をバックグラウンドスレッドで実行し、UIスレッドをブロックすることなく、UIを通常操作できるように保つことができます。

このように多くの処理を同時にフリーズすることなく使用できるInvokeは、マルチスレッドInvokeメソッドを使用するとき、異なるスレッド間でのデータのやり取りや処理の委譲が容易になります。異なるスレッドで実行されるコードを切り替えることができるようになり、マルチスレッドプログラミングの柔軟性を高めることができます。

このように、Invokeメソッドを使用することで、UIスレッドなどでの処理中に別のスレッドが同じUIリソースを待ち続けるデッドロックの問題を回避することができます。Invokeメソッドを使用することで、UIスレッドでの処理が完了するまで、他のスレッドがブロックされることなく

使うシーンとサンプル

UIフォームを表示させた状態で、デッドロックなしにC#でスレッドを生成して処理を行うと、その処理状態やデータなどをフォーム上に表示できるようになります。

以下は、サブスレッドからフォームに表示するサンプルで、スレッドを生成してサブスレッドでカウントアップしたカウンタをフォームに表示しているコードです。

基本コード
public partial class Form1 : Form
{
    Thread thread;
    public Form1()
    {
      InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
    //
    count = 0;
    label1.Text = "";
    // スレッドを生成して開始する
    thread = new Thread(new ThreadStart(ThreadProc));
    thread.Start();
    UpdateText();
    }

    private void btnExit_Click(object sender, EventArgs e)
    {
    // サブスレッドは終了させる
    thread.Abort();
    thread.Join();
    // フォームを閉じてプログラムを終了します
    this.Close();
    }

    private int count;
    private void UpdateText()
    {
        label1.Text = string.Format("{0}", count);
    }

    private void ThreadProc()
    {
        // サブスレッドの処理
        while(true)
        {
            // カウンタの値をフォームに表示する
            UpdateText();
            count++;
            if(count >= int.MaxValue)
            {
                count = int.MinValue;
            }
            System.Threading.Thread.Sleep(20);
       }
    } 
}
問題

このコードはビルドできて実行すると起動できますが、サブスレッドからUpdateText()というメソッドを呼んだ途端にInvalidOperationExceptionという例外が発生して動かなくなります。
フォームのクラス(UpdateText()はForm1クラス)メンバなので、普通に動いてもよさそうですが、実際には動いてくれません。

理由

UpdateText()というメソッドを呼ぶ「スレッド」が異なるからです。
(ですので、このUpdateText()はフォームを表示しているスレッドで呼び出すと普通に動作します)

しかしこのプログラムの場合、サブスレッドで値を変更しているので、サブスレッドから画面表示を呼びたいと考えます。

改善:サブスレッドから画面表示ができるようにする

実は「フォームを表示しているスレッド以外のスレッドからフォーム(コントロール)の操作ができない」のではなく、「フォームを表示しているスレッド以外のスレッドからフォーム(コントロール)の操作をするときのための手順が必要」です。

ControlクラスのメンバにInvokeメソッドと呼ばれるメソッドがあり、このメソッド経由で呼び出すことでフォームを表示しているスレッド以外のスレッドからフォーム(コントロール)の操作を行うことが可能になります。

改善したコード
public partial class Form1 : Form
{
    Thread thread;
    public Form1()
    {
        InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        count = 0;
        label1.Text = "";
        // スレッドを生成して開始する
        thread = new Thread(new ThreadStart(ThreadProc));
        thread.Start();
        UpdateText();
    }
    private void btnExit_Click(object sender, EventArgs e)
    {
     // サブスレッドは終了させる
        thread.Abort();
        thread.Join();
     // フォームを閉じてプログラムを終了します
        this.Close();
    }
    private int count;
    public delegate void DelegateUpdateText();
    private void OnUpdateText()
    {
        label1.Text = string.Format("{0}", count);
    }
    private void UpdateText()
    {
        //label1.Text = string.Format("{0}", count);
        this.Invoke(new DelegateUpdateText(this.OnUpdateText));
    }

    private void ThreadProc()
    {
        // サブスレッドの処理
        while(true)
        { 
        //カウ ンタの値をフォームに表示する
        UpdateText();
        count++;
        if(count >= int.MaxValue)
        {
             count = int.MinValue;
        }
        System.Threading.Thread.Sleep(20);
        }
     }
}

このコードは確かに動くのですが、「Invokeを呼び出すだけのメソッドがある。」というはちょっと気になります。(実際、フォームを表示しているスレッドから呼び出したときは、Invokeメソッドを使う必要はありません)

改善 複数のスレッドから一つのメソッドで画面表示ができるようにする

できることならこの2つのスレッドから画面表示を行うメソッドは1か所で、Invokeを呼ぶためだけのメソッドが存在するというのは避けたいところです。

実現する方法
  1. フォームを表示しているスレッド以外から呼び出されたらInvokeで処理する
  2. フォームを表示しているスレッドであればそのまま画面表示処理を行う

という実装に変更します。

改善したコード
public partial class Form1 : Form
{
    Thread thread;
    public Form1()
    {
       InitializeComponent();
    }
       private void Form1_Load(object sender, EventArgs e)
    {
    //
    count = 0;
    label1.Text = "";
     // スレッドを生成して開始する
    thread = new Thread(new ThreadStart(ThreadProc));
    thread.Start();
    UpdateText();
   }

   private void btnExit_Click(object sender, EventArgs e)
   {
      // サブスレッドは終了させる
      thread.Abort();
      thread.Join();
      // フォームを閉じてプログラムを終了します
     this.Close();
  }
  private int count;
  public delegate void DelegateUpdateText();
  private void UpdateText()
  {
      //label1.Text = string.Format("{0}", count);
     if(this.InvokeRequired)
     {
         this.Invoke(new DelegateUpdateText(this.UpdateText));
         return;
     }
     label1.Text = string.Format("{0}", count);
 }
 private void ThreadProc()
 {
   // サブスレッドの処理
    while(true)
    {
         // カウンタの値をフォームに表示する
        UpdateText();
        count++;
        if(count >= int.MaxValue)
        {
            count = int.MinValue;
        }
        System.Threading.Thread.Sleep(20);
    }
  }
}

Invokeを使う注意点

C#でInvokeメソッドを使用する際の注意点は、

別のスレッドからUIスレッドに処理を委譲することができますが、InvokeメソッドはUIスレッドで完了するまでブロックされるため、処理が長時間かかる場合にはUIがレスポンシブでなくなる可能性があることです。そのため、UIスレッドとの同期に注意し、処理が長時間かかる場合には、代わりにBeginInvokeTask.Runなどの非同期のメソッドを検討することが重要です。

また、 UIスレッド以外のスレッドからUIを更新する際には、クロススレッドの例外が発生する可能性があります。Invokeメソッドを使用する際には、UIの更新が必要なコードをInvokeの中に含めることで、UIスレッドで安全に実行されるようにする必要があります。

さらに、 Invokeメソッドを使用する際には、デッドロックのリスクに注意する必要があります。UIスレッドでの処理中に別のスレッドが同じUIリソースを待ち続けることでデッドロックが発生する可能性がありますので、UIスレッドとの同期に注意し、可能な限りUIスレッドでの処理を迅速に完了させるようにすることが重要です。

リソースの適切な管理も必要です。 Invokeメソッドを使用すると、UIスレッドでの処理が発生するため、UIスレッドでファイルやネットワークリソースへのアクセスを行う場合には、適切なロックや排他制御を実装する必要があります。

最後に、 InvokeメソッドはUIスレッドとの同期が発生するため、処理が遅くなる可能性があります。過度に頻繁にInvokeメソッドを使用すると、UIのレスポンス性が低下する可能性があるため、パフォーマンスの影響についても注意しましょう。

タイトルとURLをコピーしました