この記事では主に、C# フォーム プログラムでよく発生する状況、つまりフォームを終了するときに終了例外が発生する状況について説明します。
作者への連絡とグループへの参加方法(グループ内でアクティベーションコードが発行されます):協力 - HslCommunication 公式サイト
技術的なディスカッションへようこそ
まず、PLC またはリモート サーバーからデータが定期的に取得され、インターフェイス上のラベルが基本的にリアルタイムで更新される典型的なシナリオを見てみましょう。モデルをフォームに単純化し、スレッドを開いて定期的に読み取ることができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
public partial class Form1 : Form {
public Form1( ) {
InitializeComponent( ); } private void Form1_Load( object sender, EventArgs e ) {
thread = new System.Threading.Thread( new System.Threading.ThreadStart( ThreadCapture ) ); thread.IsBackground = true ; thread.Start( ); } private void ThreadCapture( ) {
System.Threading.Thread.Sleep( 200 ); while ( true ) {
// 我们假设这个数据是从PLC或是远程服务器获取到的,因为可能比较耗时,我们放在了后台线程获取,并且处于一直运行的状态 // 我们还假设获取数据的频率是200ms一次,然后把数据显示出来 int data = random.Next( 1000 ); // 接下来是跨线程的显示 Invoke( new Action( ( ) => {
label1.Text = data.ToString( ); } ) ); System.Threading.Thread.Sleep( 200 ); } } private System.Threading.Thread thread; private Random random = new Random( ); } } |
これを書く可能性が非常に高いのですが、ウィンドウをクリックすると、次の例外が表示されます。
この問題の根本原因は、クリックしてフォームを閉じると、フォームのすべてのコンポーネントがリソースの解放を開始しますが、スレッドがすぐには閉じられていないため、申し立てられたコードを最適化しましょう。
1. デリゲートインスタンスの継続的な作成を最適化する
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
private void ThreadCapture( ) {
showInfo = new Action< string >( m => {
label1.Text = m; } ); System.Threading.Thread.Sleep( 200 ); while ( true ) {
// 我们假设这个数据是从PLC或是远程服务器获取到的,因为可能比较耗时,我们放在了后台线程获取,并且处于一直运行的状态 // 我们还假设获取数据的频率是200ms一次,然后把数据显示出来 int data = random.Next( 1000 ); // 接下来是跨线程的显示 Invoke( showInfo, data.ToString( ) ); System.Threading.Thread.Sleep( 200 ); } } private Action< string > showInfo; private System.Threading.Thread thread; private Random random = new Random( ); |
これにより、200 ミリ秒ごとのデリゲート インスタンスの頻繁な作成が回避されます。これらのデリゲート インスタンスは、GC がデータをリサイクルするときにメモリを消費します。ユーザーは感じないかもしれませんが、適切な開発習慣により、多くのことを実行するために使用するメモリが少なくなります。
例外終了を回避する方法を考え始めたところ、nullのエラーが報告されるので判定を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
private void ThreadCapture( ) {
showInfo = new Action< string >( m => {
label1.Text = m; } ); System.Threading.Thread.Sleep( 200 ); while ( true ) {
// 我们假设这个数据是从PLC或是远程服务器获取到的,因为可能比较耗时,我们放在了后台线程获取,并且处于一直运行的状态 // 我们还假设获取数据的频率是200ms一次,然后把数据显示出来 int data = random.Next( 1000 ); // 接下来是跨线程的显示 if (IsHandleCreated && !IsDisposed) Invoke( showInfo, data.ToString( ) ); System.Threading.Thread.Sleep( 200 ); } } |
インターフェイスを表示するときに、ハンドルが作成されているかどうか、および現在解放されているかどうかを判断します。異常が発生する頻度は低くなりますが、それでも発生します。以下は簡単かつ大雑把なアイデアですが、エラーレポートが公開されているので例外をキャッチしてみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
private void ThreadCapture( ) {
showInfo = new Action< string >( m => {
label1.Text = m; } ); System.Threading.Thread.Sleep( 200 ); while ( true ) {
// 我们假设这个数据是从PLC或是远程服务器获取到的,因为可能比较耗时,我们放在了后台线程获取,并且处于一直运行的状态 // 我们还假设获取数据的频率是200ms一次,然后把数据显示出来 int data = random.Next( 1000 ); try {
// 接下来是跨线程的显示 if (IsHandleCreated && !IsDisposed) Invoke( showInfo, data.ToString( ) ); } catch (ObjectDisposedException) {
break ; } catch {
throw ; } System.Threading.Thread.Sleep( 200 ); } } |
シンプルで大雑把な解決策ですが、やはりあまり良くないと感じたので、try..catch を使う代わりに、フォームが表示されているかどうかを示すマークを自分で追加してみました。フォームを閉じるとこのマークがリセットされます
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
private void ThreadCapture( ) {
showInfo = new Action< string >( m => {
label1.Text = m; } ); isWindowShow = true ; System.Threading.Thread.Sleep( 200 ); while ( true ) {
// 我们假设这个数据是从PLC或是远程服务器获取到的,因为可能比较耗时,我们放在了后台线程获取,并且处于一直运行的状态 // 我们还假设获取数据的频率是200ms一次,然后把数据显示出来 int data = random.Next( 1000 ); // 接下来是跨线程的显示 if (isWindowShow) Invoke( showInfo, data.ToString( ) ); else break ; System.Threading.Thread.Sleep( 200 ); } } private bool isWindowShow = false ; private Action< string > showInfo; private System.Threading.Thread thread; private Random random = new Random( ); private void Form1_FormClosing( object sender, FormClosingEventArgs e ) {
isWindowShow = false ; } |
プログラム全体はこうなりました。何度テストしても頻繁に発生します。この時点で、より深いことを検討する必要があります。プログラムが終了した場合はどうすればよいですか? コレクション スレッドをオフにし、更新を停止します。そうして初めて、真に終了できます。
このとき、同期技術が必要とされ、私たちは変革を続けています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
private void ThreadCapture( ) {
showInfo = new Action< string >( m => {
label1.Text = m; } ); isWindowShow = true ; System.Threading.Thread.Sleep( 200 ); while ( true ) {
// 我们假设这个数据是从PLC或是远程服务器获取到的,因为可能比较耗时,我们放在了后台线程获取,并且处于一直运行的状态 // 我们还假设获取数据的频率是200ms一次,然后把数据显示出来 int data = random.Next( 1000 ); // 接下来是跨线程的显示,并检测窗体是否关闭 if (isWindowShow) Invoke( showInfo, data.ToString( ) ); else break ; System.Threading.Thread.Sleep( 200 ); // 再次检测窗体是否关闭 if (!isWindowShow) break ; } // 通知主界面是否准备退出 resetEvent.Set( ); } private System.Threading.AutoResetEvent resetEvent = new System.Threading.AutoResetEvent( false ); private bool isWindowShow = false ; private Action< string > showInfo; private System.Threading.Thread thread; private Random random = new Random( ); private void Form1_FormClosing( object sender, FormClosingEventArgs e ) {
isWindowShow = false ; resetEvent.WaitOne( ); } } |
このアイデアに基づいて、このコードを作成しました。しばらく実行すると結果が固まってしまいますが、これはクリックして終了するだけでInvokeが表示されているとデッドロックが発生してしまうため、1つ目の方法は以下の仕組みを変更することです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
private void ThreadCapture( ) {
showInfo = new Action< string >( m => {
label1.Text = m; } ); isWindowShow = true ; System.Threading.Thread.Sleep( 200 ); while ( true ) {
// 我们假设这个数据是从PLC或是远程服务器获取到的,因为可能比较耗时,我们放在了后台线程获取,并且处于一直运行的状态 // 我们还假设获取数据的频率是200ms一次,然后把数据显示出来 int data = random.Next( 1000 ); // 接下来是跨线程的显示,并检测窗体是否关闭 if (isWindowShow) BeginInvoke( showInfo, data.ToString( ) ); else break ; System.Threading.Thread.Sleep( 200 ); // 再次检测窗体是否关闭 if (!isWindowShow) break ; } // 通知主界面是否准备退出 resetEvent.Set( ); } private System.Threading.AutoResetEvent resetEvent = new System.Threading.AutoResetEvent( false ); private bool isWindowShow = false ; private Action< string > showInfo; private System.Threading.Thread thread; private Random random = new Random( ); private void Form1_FormClosing( object sender, FormClosingEventArgs e ) {
isWindowShow = false ; resetEvent.WaitOne( ); } |
Invoke 時に非同期機構に変更することでこの問題は解決できますが、BeginInvoke メソッドは特に安全な方法ではなく、終了時にしばらくスタックする可能性があるため、より良い方法がないか検討する必要があります。
待出口を作った方が良いのではないか?メインウィンドウに引っかかることなく、綺麗に終了できるので、新しい小ウィンドウを作成しましょう。
境界線を削除し、インターフェイスは非常にシンプルなので、コードを変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public partial class FormQuit : Form {
public FormQuit( Action action ) {
InitializeComponent( ); this .action = action; } private void FormQuit_Load( object sender, EventArgs e ) {
} // 退出前的操作 private Action action; private void FormQuit_Shown( object sender, EventArgs e ) {
// 调用操作 action.Invoke( ); Close( ); } } |
終了前の操作を終了ウィンドウに渡すだけです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
private void ThreadCapture( ) {
showInfo = new Action< string >( m => {
label1.Text = m; } ); isWindowShow = true ; System.Threading.Thread.Sleep( 200 ); while ( true ) {
// 我们假设这个数据是从PLC或是远程服务器获取到的,因为可能比较耗时,我们放在了后台线程获取,并且处于一直运行的状态 // 我们还假设获取数据的频率是200ms一次,然后把数据显示出来 int data = random.Next( 1000 ); // 接下来是跨线程的显示,并检测窗体是否关闭 if (isWindowShow) Invoke( showInfo, data.ToString( ) ); else break ; System.Threading.Thread.Sleep( 200 ); // 再次检测窗体是否关闭 if (!isWindowShow) break ; } // 通知主界面是否准备退出 resetEvent.Set( ); } private System.Threading.AutoResetEvent resetEvent = new System.Threading.AutoResetEvent( false ); private bool isWindowShow = false ; private Action< string > showInfo; private System.Threading.Thread thread; private Random random = new Random( ); private void Form1_FormClosing( object sender, FormClosingEventArgs e ) {
FormQuit formQuit = new FormQuit( new Action(()=> {
isWindowShow = false ; resetEvent.WaitOne( ); } )); formQuit.ShowDialog( ); } } |
この時点で、プログラムには上記のオブジェクトが解放されたという例外がなくなり、終了フォームには不規則な時間 (0 ~ 200 ミリ秒) が表示されます。明らかな持続効果を得るために、さらに 200 ミリ秒スリープし、次のように最終プログラムを完成させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
public partial class Form1 : Form {
public Form1( ) {
InitializeComponent( ); } private void Form1_Load( object sender, EventArgs e ) {
thread = new System.Threading.Thread( new System.Threading.ThreadStart( ThreadCapture ) ); thread.IsBackground = true ; thread.Start( ); } private void ThreadCapture( ) {
showInfo = new Action< string >( m => {
label1.Text = m; } ); isWindowShow = true ; System.Threading.Thread.Sleep( 200 ); while ( true ) {
// 我们假设这个数据是从PLC或是远程服务器获取到的,因为可能比较耗时,我们放在了后台线程获取,并且处于一直运行的状态 // 我们还假设获取数据的频率是200ms一次,然后把数据显示出来 int data = random.Next( 1000 ); // 接下来是跨线程的显示,并检测窗体是否关闭 if (isWindowShow) Invoke( showInfo, data.ToString( ) ); else break ; System.Threading.Thread.Sleep( 200 ); // 再次检测窗体是否关闭 if (!isWindowShow) {System.Threading.Thread.Sleep(50); break ;} } // 通知主界面是否准备退出 resetEvent.Set( ); } private System.Threading.AutoResetEvent resetEvent = new System.Threading.AutoResetEvent( false ); private bool isWindowShow = false ; private Action< string > showInfo; private System.Threading.Thread thread; private Random random = new Random( ); private void Form1_FormClosing( object sender, FormClosingEventArgs e ) {
FormQuit formQuit = new FormQuit( new Action(()=> {
System.Threading.Thread.Sleep( 200 ); isWindowShow = false ; resetEvent.WaitOne( ); } )); formQuit.ShowDialog( ); } } |