none
フォームクローズと更新が重なると異常終了する RRS feed

  • 質問

  • 環境: Visual stdio 2015 C#

    フォームアプリで、カウントダウン用の表示を行っていますが、表題のように、Form内のデータ表示と、フォームのクローズ重なるタイミングで異常終了する事があります。現象としては、ほぼ、把握できたのですが、どう対応すべきか困っています。

    発生するパターン:

     ClassA(コントロールするためのクラス): ここで、次のフォームを作成、制御する。
                  public event Action EventInterval;
                  定期的に、EventInterval?.Invoke(); で呼出し、下記フォーム表示の更新を行う。

      FormA(カウントダウン表示用フォーム)
                  ClassA.EventInterval には、: TextBoxの表示更新(値設定)用メソッドを設定
                  Closed/Closing イベントで、ClassA.EventInterval から、設定メソッドを削除。

      上記の設定で、画面更新(EventInterval?.Invoke()) と画面クローズが重なると、
      異常終了する。 (異常終了の内容: TextBoxの参照が失敗)
            ここで、呼出し元の EventInterval を見ると、nullになっている

    大変、分かりにくく申し訳ないですが、このような場合の対応方法について分かる方がいましたら、教えてくださるようお願いします。    

    2020年2月17日 12:57

回答

  • SynchronizationContextの使い方がもう一つなのですが、このような場合に有効でしょうか?

    Windows Forms の場合、「最初の Control 作成時」か、「Application.Run 実行時」に一度設定され、メッセージループを完全に抜けるまでは、同期用の Control を持った、WindowsFormsSynchronizationContext が設定されています。
    このため、フォームを閉じる瞬間の同期の際、Invoke/BeginInvoke 内で例外が発生する…といったリスクは減ります。
    もちろん、SynchronizationContext.Send/Post の先で IsDisposed チェックが必要なのは変わりません。

    今、ObjectDisposedException例外を読み捨てで暫定対応。
    ところで、"無限待ち"とはどのような場合でしょうか?

    以前は Invoke の先の実態、MarshaledInvoke やその先で、処理完了待ちのところで PostMessage が呼ばれた後、ウィンドウの生死チェックがなかったので、タイミング次第では無限待ち(Invoke から帰ってこない)といったことがあったはずですが、最近のコード を見る限りは、大丈夫そうです。

    なお、前述のハイパーリンク先を見るとわかりますように、ObjectDisposedException は Invoke の内部実装で発生するので、Control.IsHandleCreated や Control.InvokeRequired、Control.IsDisposed といった事前チェック方式では回避できませんので、try-catch は必要ですね。
    (Invoke の内部実装で PostMessage して待ち受けループに入る前後で、相手先ウィンドウ(フォーム)の Destroy/Dispose に至ると起きるので、スレッドが並走する限りは、事前チェック方式では回避不可です)
    2020年2月18日 12:52
    モデレータ

すべての返信

  • いまいち何やっているのか説明不足気味なので推測しかできないですが…

    更新が重なると書いてありますが、シングルスレッドならあり得ないことですから、マルチスレッドという事ですよね?
    マルチスレッドだとして、「TextBoxの参照が失敗」ということは正常時にはControl.Invoke/BeginInvokeでUIスレッドに切り替えてTextBoxを操作しようとした処理でエラーが発生しているという事ですよね?
    そうであればTextBoxは必ずUIスレッドから操作されることになるので、重なるという事はあり得ないことになります。

    にもかかわらずTextBoxの参照で失敗するという事は、TextBoxが削除された後にEventIntervalから呼びだしている処理を実行してしまっているという事です。

    ならば、Formを閉じるよりも前に別スレッドからの呼び出しイベントをきちんと解除し、BeginInvokeを行っているならBeginInvokeで呼ばれる処理ではCloseされているなら余計な処理を実行しないようにすればいいです。

    それでもエラーが出るなら、AppDomain.CurrentDomain.UnhandledExceptionで例外を捕まえて、その例外がどこでどうして発生しているのか調べましょう。

    これらよりももっと簡単なのはマルチスレッドにせずにForms.Timerを使ってUIスレッド以外から更新操作をしないことです。


    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    2020年2月17日 16:28
  • コメント、ありがとうございます。

    指摘の通り、マルチスレッドからの更新で、invoke()を使っていますが、TextBox更新時に、フォームの×ボタンで、クローズすると、問題が発生します。(クローズ時以外は問題無し)

    ちょっと時間が無いので、あとで追記しますが、とりあえず、ここまで。

    2020年2月17日 23:03
  • Invoke とありますが、Form や Control の Invoke/BeginInvoke も使っていますか?
    そうであれば、フォームが閉じる前後の瞬間に呼ばれると例外が発生するか、無限待ちになることを避けられません。(簡単な小細工では回避できません)
    SynchronizationContext を使うようにした方がベターだとは思います。
    2020年2月18日 3:27
    モデレータ
  • 現象再現できるサンプルコードが見られるならあれですが、
    以下の組み合わせで回避できるかどうかもやってみたいですね~。


    ・インスタンスが準備完了したかどうか、または、インスタンスが破棄されていないかどうか
    Control.IsHandleCreatedプロパティ

    ・(今現在別スレッド上にいるとしたら)UI スレッドに切り替えてアクセスするべきかどうか
    Control.InvokeRequiredプロパティ

    ・コントロールの null チェック

    ・Control.IsDisposed プロパティ
    2020年2月18日 9:16
  • Azuleanさん、コメントありがとうございます。

    > Invoke とありますが、Form や Control の Invoke/BeginInvoke も使っていますか?
    使っています。(Invoke)

    > SynchronizationContext を使うようにした方がベターだとは思います。
    SynchronizationContextの使い方がもう一つなのですが、このような場合に有効でしょうか?

    今、ObjectDisposedException例外を読み捨てで暫定対応。
    ところで、"無限待ち"とはどのような場合でしょうか?

    2020年2月18日 11:50
  • sutefu7さん、コメントありがとうございます。

    > Control.IsHandleCreatedプロパティ
    こちらは分かりません。調べてみたいと思います。

    > Control.InvokeRequiredプロパティ
    これは確認してます。(間違えると別の例外なので)

    > コントロールの null チェック
    これは、 "EventInterval?.Invoke();"の呼び出しなので、問題無いと思います。

    > Control.IsDisposed プロパティ
    ここで、OKでもその後、アウト。微妙なタイミングみたいです。

    2020年2月18日 11:54
  • SynchronizationContextの使い方がもう一つなのですが、このような場合に有効でしょうか?

    Windows Forms の場合、「最初の Control 作成時」か、「Application.Run 実行時」に一度設定され、メッセージループを完全に抜けるまでは、同期用の Control を持った、WindowsFormsSynchronizationContext が設定されています。
    このため、フォームを閉じる瞬間の同期の際、Invoke/BeginInvoke 内で例外が発生する…といったリスクは減ります。
    もちろん、SynchronizationContext.Send/Post の先で IsDisposed チェックが必要なのは変わりません。

    今、ObjectDisposedException例外を読み捨てで暫定対応。
    ところで、"無限待ち"とはどのような場合でしょうか?

    以前は Invoke の先の実態、MarshaledInvoke やその先で、処理完了待ちのところで PostMessage が呼ばれた後、ウィンドウの生死チェックがなかったので、タイミング次第では無限待ち(Invoke から帰ってこない)といったことがあったはずですが、最近のコード を見る限りは、大丈夫そうです。

    なお、前述のハイパーリンク先を見るとわかりますように、ObjectDisposedException は Invoke の内部実装で発生するので、Control.IsHandleCreated や Control.InvokeRequired、Control.IsDisposed といった事前チェック方式では回避できませんので、try-catch は必要ですね。
    (Invoke の内部実装で PostMessage して待ち受けループに入る前後で、相手先ウィンドウ(フォーム)の Destroy/Dispose に至ると起きるので、スレッドが並走する限りは、事前チェック方式では回避不可です)
    2020年2月18日 12:52
    モデレータ
  • Azuleanさん、説明、ありがとうございます。

    > もちろん、SynchronizationContext.Send/Post の先で IsDisposed チェックが必要

    > Control.IsHandleCreated や Control.InvokeRequired、Control.IsDisposed といった事前チェック方式では回避できませんので、try-catch は必要ですね。

    という事は、Try-catch で十分ということでしょうか?
    クローズ時のTextBox更新は当然、意味が無く、読み捨てでよいので。

    色々とありがとうございます。

    2020年2月18日 13:32
  • そうですね、今回のケースでは try-catch でなかったことにすることが一番楽だと思います。
    2020年2月19日 3:22
    モデレータ