DOBON.NET DOBON.NETプログラミング掲示板過去ログ

別スレッドからのフォーム操作中にフォームを閉じると

環境/言語:[Vista VS2005 C# .NET Framework2.0]
分類:[.NET]

お世話になります。
早速ですが、別スレッドからのフォーム操作中にフォームを閉じると
ObjectDisposedException例外が発生し、対策に悩んでおります。
毎回発生するわけではなくタイミングです。

状況
 フォームに配置したWMPコンポーネントの再生中の時間を監視したいので、
 System.Threading.TimerでWMPコンポーネントのcurrentPositionを取得しました。
 この取得は「別スレッドからのフォーム操作」なので、Invokeを使用して下記のソースを書きました。
--------------------------------------------------------------------------------------------------------------
private delegate double GetCurrentPositionDelegate();
public double GetCurrentPosition()
{
 Console.WriteLine("--- GetCurrentPosition start:[{0}] --- ", Thread.CurrentThread.ManagedThreadId);
 double pos;
 if (InvokeRequired)
 {
  pos = (double)Invoke(new GetCurrentPositionDelegate(GetCurrentPosition));
 }
 else
 {
  pos = axWindowsMediaPlayer1.Ctlcontrols.currentPosition;
 }
 Console.WriteLine("--- GetCurrentPosition end :[{0}] --- ", Thread.CurrentThread.ManagedThreadId);
 return pos;
}
// 状況トレース用
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
 System.Console.WriteLine("--- Form1_FormClosing:[{0}] --- ", Thread.CurrentThread.ManagedThreadId);
}
// 状況トレース用
protected override void Dispose(bool disposing)
{
 Console.WriteLine("--- Dispose start:[{0}] --- ", Thread.CurrentThread.ManagedThreadId);
 if (disposing && (components != null))
 {
  components.Dispose();
 }
 base.Dispose(disposing);
 Console.WriteLine("--- Dispose end :[{0}] --- ", Thread.CurrentThread.ManagedThreadId);
}
--------------------------------------------------------------------------------------------------------------

デバッグ出力
 スレッド 0xc84 はコード 0 (0x0) で終了しました。
 スレッド 0xe6c はコード 0 (0x0) で終了しました。
 スレッド 0x9e0 はコード 0 (0x0) で終了しました。
 --- GetCurrentPosition start:[10] ---
 --- GetCurrentPosition start:[8] ---
 --- GetCurrentPosition end :[8] ---
 --- GetCurrentPosition end :[10] ---
 --- GetCurrentPosition start:[10] ---
 'System.ObjectDisposedException' の初回例外が System.Windows.Forms.dll で発生しました。
 --- Form1_FormClosing:[8] ---
 --- Dispose start:[8] ---
 ---破棄されたオブジェクトにアクセスできません。
 オブジェクト名 'Form1' です。:
  場所 System.Windows.Forms.Control.MarshaledInvoke(Control caller, Delegate method, Object[] args, Boolean synchronous)
  場所 System.Windows.Forms.Control.Invoke(Delegate method, Object[] args)
  場所 System.Windows.Forms.Control.Invoke(Delegate method)
  場所 Sample.Form1.GetCurrentPosition() 場所 E:\WORKSPASE\C#\2008_10\Sample\Sample\Form1.cs:行 50
  場所 Sample.Form1.TimerThreadProc(Object state) 場所 E:\WORKSPASE\C#\2008_10\Sample\Sample\Form1.cs:行 35
 --- Dispose end :[8] ---
 スレッド 0x11ec はコード 0 (0x0) で終了しました。
 スレッド 0x1354 はコード 0 (0x0) で終了しました。
 プログラム '[4028] Sample.vshost.exe: マネージ' はコード 0 (0x0) で終了しました。


不明点
 デバッグ出力から、GetCurrentPositionはタイマースレッドでの実行後にメインスレッドで実行しています。
 これはInvokeを介して行われているので問題ありません。
 問題は「タイマースレッドでの実行」と「メインスレッドでの実行」の間にフォームを閉じたので、
 フォームが破棄され「メインスレッドでの実行」を行う際にObjectDisposedExceptionが発生することです。
 フォームが破棄されているのでIsDisposedなどでのチェックができない為、対策が出来ない状況です。

 記述したソースはネットを参考に記載しており、あちこちで見かけたので一般的な書き方だと思うのですが、
 ObjectDisposedExceptionについて記載しているサイトを見つけることが出来ませんでした。

 フォームを閉じるときに発生する例外なので、try〜catchで握り潰すことも考慮していますが、
 出来れば何らかの対策を行いたいと思っております。

 よろしくお願いいたします
2008/10/12(Sun) 19:53:52 編集(投稿者)

VB2008で試してみたのですが、FormのIsDisposedプロパティがFalseなのに
ObjectDisposedExceptionがスローされてしまうタイミングがあるようです
(Overrides OnFormClosingメソッド内で1000ミリ秒時間待ちをしている部分)。
<追記>
↑OnFormClosingメソッド終了後にInvokeメソッドが実行されるので、
「タイミング」という表現は不適切だったかもしれません。m(_ _)m
</追記>
ありきたりな対策しか思いつきませんでしたが、
フラグ変数で判定すれば回避できるようです。
 
Imports System.Threading
 
' スタートアップフォームに指定
Public Class DisposeTestLauncher
    Inherits Form
    Protected Overrides Sub OnShown(ByVal e As System.EventArgs)
        Me.Text = "ただのランチャー"
        Dim frm As New DisposeTestForm
        frm.Show()
        MyBase.OnShown(e)
    End Sub
End Class
 
Public Class DisposeTestForm
    Inherits Form
    Dim m_are As New AutoResetEvent(False)
    Dim m_closing As Boolean = False
 
    Protected Overrides Sub OnLoad(ByVal e As System.EventArgs)
        Me.Text = "このフォームを閉じる"
        ThreadPool.QueueUserWorkItem(AddressOf Me.DoWork, Nothing)
        MyBase.OnLoad(e)
    End Sub
    Private Sub DoWork(ByVal o As Object)
        m_are.WaitOne()
        ' Formを閉じるとここに来る
        Me.Disp(o)
    End Sub
    Private Sub Disp(ByVal o As Object)
        If Me.IsDisposed = True Then
            Return
        End If
        If Me.InvokeRequired = True Then
            'MsgBox(Me.IsDisposed.ToString) ' ← 結果はFalse
            If Me.IsDisposed = False Then Me.Invoke(New Action(Of Object)(AddressOf Me.Disp), o)
            ' ↓これならOKだった
            'If Me.m_closing = False Then Me.Invoke(New Action(Of Object)(AddressOf Me.Disp), o)
        Else
            Me.Text += "−追加"
        End If
    End Sub
    Protected Overrides Sub OnFormClosing(ByVal e As System.Windows.Forms.FormClosingEventArgs)
        m_closing = True
        m_are.Set()
        'MsgBox(Me.IsDisposed.ToString)' ← 結果はFalse
        ' ↓ObjectDisposedExceptionを発生しやすくするための時間待ち
        Thread.Sleep(1000)
        MyBase.OnFormClosing(e)
    End Sub
End Class
基本的にFormを閉じるタイミングでは、それにアクセスする可能性のあるスレッド全てが終了していることが望ましいかと思われます。
過去に、下記のようなスレッドがありましたし、現状でも根本的な解決はしていないはずです。

http://bbs.wankuma.com/index.cgi?mode=al2&namber=6760&KLOG=17
http://bbs.wankuma.com/index.cgi?mode=al2&namber=6843&KLOG=18


ところで、System.Windows.Forms.Timerが使えないケースだったんでしょうか?
なぜ、System.Threading.Timerにしているかが読めなかったので判断できませんでした。
回答ありがとうございます。
すみません、別件で忙しく遅くなってしまいました。

H.K.Rさんへ
 対応回避ソースの提示ありがとうございます。
 まだ、試せておりません。出来次第、ご報告いたします。

Azuleanへ
 System.Windows.Forms.Timerでの実装はできそうだったので、
 System.Threading.Timerでの実装方法を調査していて遭遇した事象でした。

以上、よろしくお願いいたします
2008/10/18(Sat) 02:33:42 編集(投稿者)

コードをバージョンアップしてみました。
System.Threading.TimerとAsyncOperation.Postメソッドを使ったところ、
Formを開く→閉じるを1万回繰り返してもエラー数0でした。(実行時間は40分程度です)
 
Imports System.ComponentModel
Imports System.Threading
 
' スタートアップFormに指定
Public Class DisposeTestLancher_ver2
    Inherits Form
    Dim WithEvents Button1 As New Button
    ' Formを表示して、一定時間経過後閉じるためのTimer
    Dim WithEvents Timer1 As New System.Windows.Forms.Timer
    Dim m_close As Boolean = False
    Dim m_count As Integer = 0
    Dim m_errorcount As Integer = 0
    Dim m_frm As Form
    Dim m_rand As New Random(Environment.TickCount Mod 1000)
 
    Protected Overrides Sub OnLoad(ByVal e As System.EventArgs)
        Me.Text = "スタートアップForm"
        Me.Button1.Text = "Start"
        Me.Controls.Add(Me.Button1)
        Me.Timer1.Enabled = False
        MyBase.OnLoad(e)
    End Sub
    Private Sub Button1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Button1.Click
        ' Formを1万回表示してエラー回数を測定
        Me.m_close = False
        Me.Timer1.Interval = 10
        Me.Timer1.Enabled = True
    End Sub
 
    Private Sub Timer1_Tick(ByVal sender As Object, ByVal e As System.EventArgs) Handles Timer1.Tick
        If Me.m_close = False Then
            Me.m_frm = New DisposeTestForm_ver2
            Me.m_frm.Show()
            Me.Timer1.Interval = Me.m_rand.Next(80, 300) ' Formを閉じるまでの時間
        Else
            Try
                If Me.m_frm IsNot Nothing Then m_frm.Close()
            Catch ex As InvalidOperationException
                ' ObjectDisposedExceptionもここに来る
                Me.m_errorcount += 1
            Finally
                Me.m_count += 1
            End Try
            ' 100回毎に経過表示
            If Me.m_count Mod 100 = 0 Then Console.WriteLine("{0}回 - エラー数 {1}", Me.m_count, Me.m_errorcount)
            Me.Timer1.Interval = 10 ' 次のFormはすぐ表示する
            If Me.m_count = 10000 Then Me.Timer1.Enabled = False ' 1万回で終了
        End If
        Me.m_close = Not Me.m_close
    End Sub
End Class
 
' 1万回表示されるForm
Public Class DisposeTestForm_ver2
    Inherits Form
    Dim m_ao As AsyncOperation = AsyncOperationManager.CreateOperation(Me)
    Dim m_IsBusy As Boolean = False
    Dim Timer1 As System.Threading.Timer
    Dim AddressOf_SetText As New SendOrPostCallback(AddressOf Me.SetText)
    Dim m_closing As Boolean = False
 
    Protected Overrides Sub OnLoad(ByVal e As System.EventArgs)
        Me.Timer1 = New System.Threading.Timer(AddressOf Me.DoWork, "あああ", 0, 50)
        Me.BackColor = Color.Blue
        MyBase.OnLoad(e)
    End Sub
    Private Sub DoWork(ByVal o As Object)
        Me.m_IsBusy = True
        ' デッドロック回避のため、非同期呼び出しを使用する
        If Me.m_closing = False Then
            m_ao.Post(Me.AddressOf_SetText, o)
        End If
        Me.m_IsBusy = False
    End Sub
    Private Sub SetText(ByVal o As Object)
        If Me.m_closing = True Then Return
        Me.Text = o.ToString
        Me.Show() ' ←閉じた後なら、ObjectDisposedExceptionになる
    End Sub
 
    Protected Overrides Sub OnFormClosing(ByVal e As System.Windows.Forms.FormClosingEventArgs)
        Me.m_closing = True
        Me.Timer1.Dispose()
        ' Timerのコールバックメソッド終了を待つ(フラグを使った苦し紛れの処理)
        Do While Me.m_IsBusy = True
            Thread.Sleep(10)
        Loop
        Me.m_ao.OperationCompleted()
        MyBase.OnFormClosing(e)
    End Sub
End Class
お世話になっております。

提示ソースの更新ありがとうございます。
本当に申し訳ございません。まだ、試せておりません。

時間を作り、絶対に試します!

以上、よろしくお願い致します。
お世話になっております。

遅くなって申し訳ございません。
やっと、少しだけ時間がとれたのでご提示頂いたver2のソースを
VB.NET⇒C#に変えて、試させて頂きました。

AsyncOperationを使用することで、メインスレッド、タイマースレッドともに、
1回で目的の操作を実行できているので、外から操作(閉じる)される確率が
格段に減ることがわかりました。
 自分の書いたソースはタイマースレッドからの操作の場合、
 Invokeを使用していたため2回処理していました。

目的の回避方法が解決しましたので、本スレッドを解決とさせて頂きます。
本当に遅くなって申し訳ございませんでした。

よろしくお願いいたします
解決済み!

DOBON.NET | プログラミング道 | プログラミング掲示板