C#のInvokeでUIスレッド例外を解決する方法

C#でWindows Formsアプリケーションを開発していると、「別スレッドから画面のコントロールを更新しようとしたら例外が発生した」というトラブルに遭遇することがあります。この記事では、その原因と、Invokeメソッドを使った解決方法を分かりやすく解説します。

こんな悩みはありませんか

  • 別スレッドでUIを更新しようとした際に発生する例外
  • 「Cross-thread operation not valid」というエラーメッセージの意味
  • 非同期処理の結果を画面へ反映させる正しい方法
  • InvokeBeginInvokeの違いや使い分け

上記のような課題を抱えている方に向けて、原因の理解から具体的な解決方法までを順を追って説明します。

なぜ例外が発生するのか

Windows Formsのコントロール(ボタンやピクチャーボックスなど)は、「作成されたスレッド(通常はメインスレッド、別名UIスレッド)からしか操作してはいけない」というルールを持っています。これはスレッドセーフではない処理を複数のスレッドから同時に実行してしまうと、画面表示が壊れたり、アプリケーションが不安定になったりする可能性があるためです。
そのため、画像処理や通信処理などを別スレッド(ワーカースレッド)で実行し、その結果をそのままUIスレッド以外からピクチャーボックスなどのコントロールに反映しようとすると、システムが例外を発生させて処理を止めます。これは「UIスレッド以外からのアクセスを禁止する」という安全装置が働いている状態です。
この問題を解決するには、「別スレッドで実行している処理から、UIスレッドに処理を依頼する」という仕組みが必要になります。それを実現するのがControl.Invoke()Form.Invoke()です。

解決方法:Control.InvokeとForm.Invokeを使う

Control.Invoke()またはForm.Invoke()を使うことで、別スレッドから呼び出した処理を、UIスレッド上で安全に実行できます。仕組みとしては、実行したい処理(デリゲートやラムダ式)をUIスレッドのメッセージキューに渡し、UIスレッドが手が空いたタイミングでその処理を実行する、というイメージです。
以下は、ピクチャーボックス(pictBox)に対してInvokeを呼び出す例です。

using OpenCvSharp;
using OpenCvSharp.Extensions;

this.pictBox.Invoke(new Action(() => {
    if (this.pictBox.Image != null) {
        this.pictBox.Image.Dispose();
    }
    this.pictBox.Image = BitmapConverter.ToBitmap(mat);
}));

コントロール単位ではなく、フォーム自体のInvokeを使っても同様の効果が得られます。

using OpenCvSharp;
using OpenCvSharp.Extensions;

this.Invoke(new Action(() => {
    if (this.pictBox.Image != null) {
        this.pictBox.Image.Dispose();
    }
    this.pictBox.Image = BitmapConverter.ToBitmap(mat);
}));

いずれの場合も、更新前に既存のImageが残っていればDispose()でメモリを解放してから、新しい画像をセットしている点がポイントです。画像を使い捨てにしたままにすると、メモリリークの原因になるため注意が必要です。

匿名メソッド(ラムダ式)を使うとよりシンプルに書ける

new Action(() => { ... })のように明示的にAction型を生成しなくても、ラムダ式をそのまま渡すことで同じ処理をより簡潔に書けます。

using OpenCvSharp;
using OpenCvSharp.Extensions;

this.Invoke(() => {
    if (this.pictBox.Image != null) {
        this.pictBox.Image.Dispose();
    }
    this.pictBox.Image = BitmapConverter.ToBitmap(mat);
});

記述量が減るだけでなく、処理内容がその場で読み取りやすくなるため、実務ではこちらの書き方がよく使われます。

Invokeを使う際に気をつけたいポイント

Invokeは便利な仕組みですが、使い方を誤ると別の問題を引き起こすことがあります。IT初心者の方が特につまずきやすいポイントを以下にまとめます。

  • Invokeは呼び出し元のスレッドが処理完了まで待機する仕組みのため、UIスレッドが別の重い処理でブロックされていると画面が固まって見えるケース
  • 完了を待たずに処理を依頼したい場合のInvokeではなくBeginInvokeを使うという選択肢
  • 呼び出し元が既にUIスレッドかどうかを判定したい場合のInvokeRequiredプロパティでの確認
  • コントロールが破棄(Dispose)された後にInvokeを呼び出すと例外が発生するため、フォームを閉じる処理と非同期処理のタイミングへの注意が必要

まとめ

C#のWindows Formsでは、UIスレッド以外からコントロールを直接操作すると例外が発生します。これは画面表示の安全性を守るための仕組みであり、Control.Invoke()Form.Invoke()を使って処理をUIスレッドに委譲することで解決できます。ラムダ式を使えばより簡潔に書けるため、まずは基本の書き方を押さえたうえで、BeginInvokeInvokeRequiredといった関連機能も合わせて理解しておくと、非同期処理まわりのトラブルを未然に防げます。

このエントリーをはてなブックマークに追加
にほんブログ村 IT技術ブログへ

コメント

メールアドレスが公開されることはありません。 が付いている欄は必須項目です