Try Catch能幫你做什麼(4)?

第四篇來談談用Try Catch來避免必然會發生的問題,一開始我在建立這個系列文章的原因是因為常再MSDN看到許多.Net同好們問的問題所引發的靈感,因為許多同好對於Try Catch並沒有特別的重視其例外訊息所帶給程式撰寫者的提示,所以往往會被這些例外狀況困住;而另一種情況則是引發了必然發生的例外,卻沒想到使用Try Catch來閃躲這個例外,這也就是這一篇的主要討論範圍。

        第四篇來談談用Try Catch來避免必然會發生的問題,一開始我在建立這個系列文章的原因是因為常在MSDN看到許多.Net同好們問的問題所引發的靈感,因為許多同好對於Try Catch並沒有特別的重視其例外訊息所帶給程式撰寫者的提示,所以往往會被這些例外狀況困住;而另一種情況則是引發了必然發生的例外,卻沒想到使用Try Catch來閃躲這個例外,這也就是這一篇的主要討論範圍。

       首先,先介紹一個非常簡單的多執行緒程式碼:           

Imports System.Threading

Public Class Form1
    Dim myThread As New Thread(AddressOf LoopTest)
    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        EventLog1.Source = "TreadTest"
    End Sub
    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

        myThread.Start()
    End Sub
    Private Sub LoopTest(ByVal state As Object)
        While True
            Try
                Application.DoEvents()
                System.Threading.Thread.Sleep(1000)
                EventLog1.WriteEntry(Now().ToString("yyyy/MM/dd HH:mm:ss") & ":進入次執行緒")
            Catch ex As Exception
                EventLog1.WriteEntry(Now().ToString("yyyy/MM/dd HH:mm:ss") & ex.ToString, System.Diagnostics.EventLogEntryType.Error)
            End Try

        End While
    End Sub

End Class

       這個程式碼畫面上只有一個Button控制項和一個EventLog元件,執行一個很簡單的動作,就是當你按下Button1,程式就會加入一個執行緒LoopTest,執行一個無窮迴圈的行為,每隔一秒中將資料寫入EventLog中。

        EventLog的設定

Try_Catch_4_Pic02

        當我們完成了這個程式後,進行編譯建置,然後到輸出目錄執行這個程式,按下Button1,觀察在事件檢視器中是否有出現我們預期的應用程式事件產生;嗯,看起來是沒問題。接著把此視窗關閉,怪事發生了,事件檢視器中依然一直冒出新的事件,這時我們來將剛剛的程式重新建置,糟了個大糕,出現了以下的錯誤訊息:

------ 已開始全部重建: 專案: ThreadTry, 組態: Debug Any CPU ------
C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Microsoft.Common.targets : warning MSB3061: 無法刪除檔案 "D:\DemoCode\VB\ThreadTry\ThreadTry\bin\Debug\ThreadTry.exe"。拒絕存取路徑 'D:\DemoCode\VB\ThreadTry\ThreadTry\bin\Debug\ThreadTry.exe'。
C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Vbc.exe /noconfig /imports:Microsoft.VisualBasic,System,System.Collections,System.Collections.Generic,System.Data,System.Drawing,System.Diagnostics,System.Windows.Forms /nowarn:42016,41999,42017,42018,42019,42032,42036,42020,42021,42022 /rootnamespace:ThreadTry /doc:obj\Debug\ThreadTry.xml /define:"CONFIG=\"Debug\",DEBUG=-1,TRACE=-1,_MyType=\"WindowsForms\",PLATFORM=\"AnyCPU\"" /reference:C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\System.Data.dll,C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\System.Deployment.dll,C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\System.dll,C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\System.Drawing.dll,C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\System.Windows.Forms.dll,C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\System.Xml.dll /main:ThreadTry.My.MyApplication /debug+ /debug:full /out:obj\Debug\ThreadTry.exe /resource:obj\Debug\ThreadTry.Form1.resources /resource:obj\Debug\ThreadTry.Resources.resources /target:winexe Form1.vb Form1.Designer.vb "My Project\AssemblyInfo.vb" "My Project\Application.Designer.vb" "My Project\Resources.Designer.vb" "My Project\Settings.Designer.vb"
C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Microsoft.Common.targets(2324,9): error MSB3021: 無法將檔案 "obj\Debug\ThreadTry.exe" 複製到 "bin\Debug\ThreadTry.exe"。由於另一個處理序正在使用檔案 'bin\Debug\ThreadTry.exe',所以無法存取該檔案。
專案 "ThreadTry.vbproj" 建置完成 -- 失敗
========== 全部重建: 0 成功、1 失敗、 0 略過 ==========

       那來觀察一下Windows工作管理員,ThreadTry.exe還在!這是怎麼一回事?原來當我們關閉主程式的時候LoopTest這個Thread並沒有如我們的預期一般的關閉。

Try_Catch_4_Pic01

       好吧,既然如此,咱們給它在Form Closing事件加個myThread.Abort()來終止執行緒總行了吧?

Private Sub Form1_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
      myThread.Abort()
End Sub

       再來執行一次,看起來似乎沒有問題,回頭來看看事件檢視器,發生了一個錯誤:Try_Catch_4_Pic03

       原來在執行Thread.Abort()方法的時候,會引發一個System.Threading.ThreadAbortException的例外,關於這個例外的說明可以參考 MSDN:Thread.Abort 方法

       現在問題來了,這個是一個必然發生的例外,但是其實是不需要特別處理的,於是我們可以將程式改為以下的形式來避免處理這個例外。

Private Sub LoopTest(ByVal state As Object)
        While True
            Try
                Application.DoEvents()
                System.Threading.Thread.Sleep(1000)
                EventLog1.WriteEntry(Now().ToString("yyyy/MM/dd HH:mm:ss") & ":進入次執行緒")
           Catch abortEx As ThreadAbortException
                '不要做任何處理
            Catch ex As Exception
                EventLog1.WriteEntry(Now().ToString("yyyy/MM/dd HH:mm:ss") & ex.ToString, System.Diagnostics.EventLogEntryType.Error)
            End Try

        End While
    End Sub

       因為這個程式不複雜,可能不足以說明避免欄截到ThreadAbortException引發一些讓使用者很疑惑的現象,想像你有一個MDI Form的程式,這個程式有一個Child Form是具備有一個無窮迴圈的次執行緒,而當使用者切換到另一個Child Form時,你有需要讓這個次執行緒終止,如果在這當時突然跳出一個例外發生的視窗,感覺還挺糗的,而且使用者未必能夠瞭解這個例外是必然會發生的。

       另一個我常用到這種攔截而不處理的現象也是和多執行緒有關,則是在跨執行緒呼叫Invoke的時候產生,讓我們再改一下程式,在畫面上多加一個Label控制項:

Imports System.Threading

Public Class Form1

    '  新增一個委派=========================

    Delegate Sub SetMsg1Callback(ByVal InputString As String)
    Private Sub DisplayMsg1(ByVal strReceive As String)
        If Me.Label1.InvokeRequired Then
            Dim d As New SetMsg1Callback(AddressOf DisplayMsg1)
            Me.Invoke(d, New Object() {strReceive})
        Else
            Me.Label1.Text = strReceive
        End If
    End Sub

    '  =======================================

    Dim myThread As New Thread(AddressOf LoopTest)
    Private Sub Form1_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
        '  myThread.Abort() <—將 Abort 註解起來
    End Sub
    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        EventLog1.Source = "TreadTest"
    End Sub
    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

        myThread.Start()
    End Sub
    Private Sub LoopTest(ByVal state As Object)
        While True
            Try
                Application.DoEvents()
                DisplayMsg1(Now().ToString("yyyy/MM/dd HH:mm:ss") & ":進入次執行緒")
            Catch ex As Exception
                EventLog1.WriteEntry(Now().ToString("yyyy/MM/dd HH:mm:ss") & ex.ToString, System.Diagnostics.EventLogEntryType.Error)
            End Try

        End While
    End Sub

      當我們關閉這個Form的時候,很有機會會引發『System.ObjectDisposedException: 無法存取已處置的物件。』這樣的例外,原因在於Form1執行個體已經被釋放了,次執行緒卻試圖要去存取已經不存在的物件,於是我們必需特別去攔截這個例外,並且不做任何例外處置。如下所示:

Catch objDis As ObjectDisposedException
              '不要做任何處理

        這個簡單的程式可以在此下載ThreadTry.rar  。

        這一篇應該是這個系列的最終章了,當然還有許許多多關於Try Catch的應用是這邊沒有談到的,也希望前輩們可以給我一些意見!

        《後記1》:我在寫完這篇之後沒多久,看到了蹂躪大大發表了一篇「例外處理使用時機」,個人覺得是對於例外處理有更深入的見解,有興趣的同好可以點選連結去閱讀。

        《後記2》:後來和蹂躪大大進行了一個有趣的討論,發現我的例子舉的不太好,如果是因為要隨著關閉Form而停止次執行緒,比較適當的方法是將 Thread.IsBackground屬性設為True,如此一來就不需要呼叫Thread.Abort()了;不過如果是以其它事件﹝ex: Button.Click﹞呼叫Thread.Abort(),我想還是需要用Try Catch來避免例外產生的訊息。而關於『System.ObjectDisposedException: 無法存取已處置的物件。』這個例外,根據測試的結果,即使先判斷了 Control.IsDisposed屬性,在這個例子中,我曾測試判斷Label或是Form的IsDisposed屬性,依然會發生例外。目前的結果顯示攔截ObjectDisposedException似乎是不可避免的。