2024年7月30日 星期二

23.VB.NET 筆記 進階篇 - BeginInvoke

VB.NET BeginInvoke 筆記 (進階篇)

VB.NET BeginInvoke 筆記 (進階篇)

BeginInvokeBeginInvoke 就像是火車站的列車調度員,負責安排列車的出發和到達,在背後默默完成許多複雜的調度工作,讓火車能夠順暢地運行。是 VB.NET 中一個非常強大且實用的機制,可以在不阻塞目前執行緒的情況下,將指定的方法委派給另一個執行緒非同步執行。透過靈活運用 BeginInvoke,可以大幅提升應用程式的效能和響應速度。

認識 BeginInvoke

BeginInvoke: BeginInvoke 是 VB.NET 中的一個方法,用於在另一個執行緒上非同步非同步就像是在火車站中,有多條軌道可以同時讓多列火車運行,不會因為一列火車的延誤而影響其他火車的正常運行。執行委派。它會立即返回,不會等待委派執行完畢,因此不會阻塞目前的執行緒。BeginInvoke 通常用於需要執行耗時操作,但又不希望阻塞使用者介面的場景。

BeginInvoke 有以下幾個重要特性:

  • 非同步執行BeginInvoke 就像是在火車站中,安排一列新的火車從另一條軌道出發,不會影響目前軌道上的火車運行。同樣地,BeginInvoke 會在另一個執行緒上執行委派,不會阻塞目前的執行緒。: BeginInvoke 會立即返回,不會等待委派執行完畢,因此不會阻塞目前的執行緒。
  • 委派型別BeginInvoke 就像是火車調度員,可以根據不同的需求來安排不同型別的列車出發。同樣地,BeginInvoke 可以接受不同型別的委派,以執行不同的任務。: BeginInvoke 可以接受不同型別的委派,例如 Sub、Function、Action、Func 等。
  • 參數傳遞BeginInvoke 就像是火車調度員,可以根據不同的需求來安排列車攜帶不同的貨物。同樣地,BeginInvoke 可以接受一個物件陣列,將參數傳遞給委派。: BeginInvoke 可以接受一個物件陣列,用於將參數傳遞給委派。
  • IAsyncResultBeginInvoke 就像是火車調度員,會記錄列車的出發和到達情況。同樣地,BeginInvoke 會返回一個 IAsyncResult 物件,用於追蹤委派的執行狀態。: BeginInvoke 會返回一個 IAsyncResult 物件,用於追蹤委派的執行狀態。

使用 BeginInvoke 可以讓應用程式更加靈活和高效。透過將耗時的操作委派給背景執行緒,可以大幅提升使用者介面的響應速度和使用者體驗。同時,由於 BeginInvoke 是非同步執行的,也可以避免因為等待耗時操作完成而造成應用程式的阻塞或停頓。

使用 BeginInvoke

在 VB.NET 中,使用 BeginInvoke 的基本語法如下:

Dim result As IAsyncResult = 控制項.BeginInvoke(委派, 參數)

其中,控制項是要執行委派的控制項或物件,委派是要執行的委派,參數是要傳遞給委派的參數陣列。BeginInvoke 會立即返回一個 IAsyncResult 物件,用於追蹤委派的執行狀態。

功能一:基本非同步執行 (簡單耗時操作)

這是 BeginInvoke 最基本的用途。當需要執行耗時操作,但又不希望阻塞使用者介面時,可以使用 BeginInvoke 將操作委派給背景執行緒執行。

使用的控制項:
  • Button1: 用於觸發耗時操作。
  • Label1: 用於顯示耗時操作的結果。
  • Label2: 用於顯示操作狀態。
範例程式碼
' 引入必要的命名空間以使用執行緒功能
Imports System.Threading

' 定義主要的表單類別
Public Class Form1
    ' 按下 Button1 時觸發的事件
    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        ' 更新狀態標籤顯示開始處理
        Label2.Text = "處理中..."
        ' 定義一個 Action 委派,指向 LongRunningTask 方法
        Dim del As New Action(AddressOf LongRunningTask)
        ' 使用 BeginInvoke 在背景執行緒上執行耗時方法
        Me.BeginInvoke(del)
        ' 更新狀態標籤顯示已啟動背景任務
        Label2.Text = "背景任務已啟動"
    End Sub
    
    ' 耗時任務的方法
    Private Sub LongRunningTask()
        ' 模擬一個耗時 3 秒的操作
        Thread.Sleep(3000)
        ' 使用 BeginInvoke 更新 Label1 的文字(必須在 UI 執行緒上執行)
        Label1.BeginInvoke(Sub()
                               ' 設定 Label1 顯示完成訊息
                               Label1.Text = "耗時操作完成"
                           End Sub)
        ' 使用 BeginInvoke 更新 Label2 的文字
        Label2.BeginInvoke(Sub()
                               ' 設定 Label2 顯示任務完成
                               Label2.Text = "任務完成"
                           End Sub)
    End Sub
End Class
詳細講解

此範例展示了 BeginInvoke 的基本用法。當按下 Button1 時,程式建立一個 Action 委派指向 LongRunningTask 方法,然後使用 Me.BeginInvoke(del) 在背景執行緒上執行這個委派。由於 BeginInvoke 是非同步的,它會立即返回,不會阻塞 UI 執行緒,因此 Label2 可以立即顯示「背景任務已啟動」。

LongRunningTask 方法中,使用 Thread.Sleep(3000) 模擬耗時操作。完成後,由於更新 UI 控制項必須在 UI 執行緒上執行,因此使用 Label1.BeginInvokeLabel2.BeginInvoke 來確保更新操作在正確的執行緒上執行。這種模式確保了執行緒安全執行緒安全是指程式碼能在多執行緒環境下正確執行,不會產生資料競爭或不一致的結果。,避免了跨執行緒操作 UI 控制項的錯誤。

功能二:帶參數的非同步執行 (傳遞資料)

BeginInvoke 可以接受參數,將資料從主執行緒傳遞到背景執行緒。這在需要根據使用者輸入進行處理的場景中非常有用。

使用的控制項:
  • TextBox1: 用於輸入訊息內容。
  • Button1: 用於觸發處理操作。
  • Label1: 用於顯示處理後的結果。
範例程式碼
' 引入必要的命名空間
Imports System.Threading

' 定義主要的表單類別
Public Class Form1
    ' 按下 Button1 時觸發的事件
    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        ' 取得 TextBox1 中的文字內容
        Dim message As String = TextBox1.Text
        ' 定義一個接受 String 參數的 Action 委派
        Dim del As New Action(Of String)(AddressOf ProcessMessage)
        ' 使用 BeginInvoke 在背景執行緒上執行委派,並傳遞參數
        Me.BeginInvoke(del, message)
    End Sub
    
    ' 處理訊息的方法,接受一個字串參數
    Private Sub ProcessMessage(message As String)
        ' 模擬一個耗時 2 秒的處理操作
        Thread.Sleep(2000)
        ' 將訊息轉換為大寫
        Dim processedMessage As String = message.ToUpper()
        ' 在訊息前後加上裝飾符號
        processedMessage = "【" & processedMessage & "】"
        ' 使用 BeginInvoke 更新 Label1 顯示處理後的訊息
        Label1.BeginInvoke(Sub()
                               ' 設定 Label1 的文字為處理後的訊息
                               Label1.Text = "處理結果: " & processedMessage
                           End Sub)
    End Sub
End Class
詳細講解

此範例展示了如何使用 BeginInvoke 傳遞參數。當按下 Button1 時,程式首先從 TextBox1 取得使用者輸入的文字,然後建立一個 Action(Of String) 委派,該委派接受一個字串參數。使用 Me.BeginInvoke(del, message) 將委派和參數一起傳遞給背景執行緒執行。

ProcessMessage 方法中,接收到的參數 message 會被處理:先模擬耗時操作,然後將文字轉換為大寫並加上裝飾符號。最後,使用 Label1.BeginInvoke 在 UI 執行緒上更新顯示結果。這種方式實現了資料從 UI 執行緒到背景執行緒的傳遞,以及處理結果從背景執行緒回到 UI 執行緒的流程,確保了執行緒間的資料交換執行緒間的資料交換需要謹慎處理,確保資料在正確的執行緒上被存取和修改。安全且正確。

功能三:配合 BackgroundWorker 的進階應用

對於需要執行長時間的後台任務,配合 BackgroundWorker 元件使用 BeginInvoke 可以提供更完善的功能,包括進度報告、取消支援和異常處理。

使用的控制項:
  • Button1: 用於開始後台任務。
  • Button2: 用於取消後台任務。
  • ProgressBar1: 用於顯示後台任務的進度。
  • Label1: 用於顯示後台任務的狀態。
範例程式碼
' 引入必要的命名空間
Imports System.ComponentModel
Imports System.Threading

' 定義主要的表單類別
Public Class Form1
    ' 宣告 BackgroundWorker 變數
    Private worker As BackgroundWorker
    
    ' 表單載入時的初始化
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        ' 建立 BackgroundWorker 實例
        worker = New BackgroundWorker()
        ' 啟用進度報告功能
        worker.WorkerReportsProgress = True
        ' 啟用取消支援功能
        worker.WorkerSupportsCancellation = True
        ' 訂閱 DoWork 事件,用於執行後台工作
        AddHandler worker.DoWork, AddressOf Worker_DoWork
        ' 訂閱 ProgressChanged 事件,用於更新進度
        AddHandler worker.ProgressChanged, AddressOf Worker_ProgressChanged
        ' 訂閱 RunWorkerCompleted 事件,用於處理完成後的操作
        AddHandler worker.RunWorkerCompleted, AddressOf Worker_RunWorkerCompleted
        ' 設定 ProgressBar1 的最大值為 100
        ProgressBar1.Maximum = 100
        ' 設定 ProgressBar1 的初始值為 0
        ProgressBar1.Value = 0
        ' 設定 Label1 的初始文字
        Label1.Text = "就緒"
    End Sub
    
    ' 按下 Button1 時開始後台任務
    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        ' 檢查 worker 是否正在執行
        If Not worker.IsBusy Then
            ' 重設進度條為 0
            ProgressBar1.Value = 0
            ' 更新狀態標籤
            Label1.Text = "處理中..."
            ' 開始執行後台任務
            worker.RunWorkerAsync()
        End If
    End Sub
    
    ' 按下 Button2 時取消後台任務
    Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
        ' 檢查 worker 是否正在執行
        If worker.IsBusy Then
            ' 請求取消後台任務
            worker.CancelAsync()
            ' 更新狀態標籤
            Label1.Text = "正在取消..."
        End If
    End Sub
    
    ' BackgroundWorker 的 DoWork 事件處理程序
    Private Sub Worker_DoWork(sender As Object, e As DoWorkEventArgs)
        ' 取得 BackgroundWorker 實例
        Dim worker As BackgroundWorker = CType(sender, BackgroundWorker)
        ' 執行 10 次迭代,模擬長時間任務
        For i As Integer = 1 To 10
            ' 檢查是否收到取消請求
            If worker.CancellationPending Then
                ' 設定取消標誌
                e.Cancel = True
                ' 退出迴圈
                Exit For
            End If
            ' 模擬耗時操作,暫停 500 毫秒
            Thread.Sleep(500)
            ' 報告進度百分比
            worker.ReportProgress(i * 10)
        Next
    End Sub
    
    ' BackgroundWorker 的 ProgressChanged 事件處理程序
    Private Sub Worker_ProgressChanged(sender As Object, e As ProgressChangedEventArgs)
        ' 使用 BeginInvoke 更新進度條(在 UI 執行緒上)
        ProgressBar1.BeginInvoke(Sub()
                                     ' 設定進度條的值
                                     ProgressBar1.Value = e.ProgressPercentage
                                 End Sub)
        ' 使用 BeginInvoke 更新狀態標籤
        Label1.BeginInvoke(Sub()
                               ' 顯示當前進度百分比
                               Label1.Text = $"處理中... {e.ProgressPercentage}%"
                           End Sub)
    End Sub
    
    ' BackgroundWorker 的 RunWorkerCompleted 事件處理程序
    Private Sub Worker_RunWorkerCompleted(sender As Object, e As RunWorkerCompletedEventArgs)
        ' 檢查任務是否被取消
        If e.Cancelled Then
            ' 使用 BeginInvoke 更新狀態標籤
            Label1.BeginInvoke(Sub()
                                   ' 顯示任務已取消
                                   Label1.Text = "任務已取消"
                               End Sub)
        ' 檢查是否有錯誤發生
        ElseIf e.Error IsNot Nothing Then
            ' 使用 BeginInvoke 更新狀態標籤
            Label1.BeginInvoke(Sub()
                                   ' 顯示錯誤訊息
                                   Label1.Text = $"發生錯誤: {e.Error.Message}"
                               End Sub)
        ' 任務正常完成
        Else
            ' 使用 BeginInvoke 更新狀態標籤
            Label1.BeginInvoke(Sub()
                                   ' 顯示任務已完成
                                   Label1.Text = "任務已完成"
                               End Sub)
        End If
    End Sub
End Class
詳細講解

此範例展示了 BeginInvoke 與 BackgroundWorker 的配合使用。在 Form1_Load 事件中,建立並配置 BackgroundWorker 實例,啟用進度報告和取消支援功能,並訂閱相關事件。

當按下 Button1 時,程式檢查 worker 是否正在執行,如果沒有則呼叫 worker.RunWorkerAsync() 開始後台任務。在 Worker_DoWork 事件中,執行實際的耗時任務,並定期檢查 CancellationPending 屬性以支援取消操作,同時使用 worker.ReportProgress 報告進度。

Worker_ProgressChanged 事件中,使用 BeginInvoke 更新 UI 控制項(ProgressBar1 和 Label1),確保更新操作在 UI 執行緒上執行。在 Worker_RunWorkerCompleted 事件中,根據任務的完成狀態(正常完成、取消或異常),使用 BeginInvoke 更新狀態標籤顯示相應訊息。

這個範例展示了如何結合 BackgroundWorker 和 BeginInvoke 來實現一個功能完整的後台任務處理系統,包括進度追蹤、取消支援和異常處理,同時保證 UI 的響應性。

BeginInvoke vs Invoke

在 VB.NET 中,除了 BeginInvoke 之外,還有另一個方法 Invoke 也可以用於跨執行緒訪問 UI 控制項。這兩個方法的主要差異在於執行方式和阻塞效果。

比較項目 BeginInvoke Invoke
執行方式 非同步執行,立即返回 同步執行,等待委派完成
阻塞效果 不阻塞呼叫執行緒 阻塞呼叫執行緒直到委派完成
返回值 返回 IAsyncResult 物件 返回委派的執行結果
使用場景 適用於執行長時間的任務
不需要等待委派完成
適用於執行快速的任務
需要取得委派的執行結果

對比範例:BeginInvoke vs Invoke

使用的控制項:
  • Button1: 用於執行使用 Invoke 的範例(會阻塞)。
  • Button2: 用於執行使用 BeginInvoke 的範例(不會阻塞)。
  • Label1: 用於顯示操作結果。
  • Label2: 用於顯示即時狀態。
範例程式碼
' 引入必要的命名空間
Imports System.Threading

' 定義主要的表單類別
Public Class Form1
    ' 按下 Button1 時使用 Invoke(同步,會阻塞)
    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        ' 更新 Label2 顯示開始處理
        Label2.Text = "開始處理(Invoke)..."
        ' 使用 Invoke 執行耗時任務(會阻塞 UI 執行緒)
        Me.Invoke(New Action(Sub()
                                 ' 模擬耗時操作 3 秒
                                 Thread.Sleep(3000)
                                 ' 更新 Label1 顯示完成訊息
                                 Label1.Text = "Invoke 執行完成"
                             End Sub))
        ' 這行程式碼要等到上面的 Invoke 完成後才會執行
        Label2.Text = "Invoke 完成(UI 被阻塞了 3 秒)"
    End Sub
    
    ' 按下 Button2 時使用 BeginInvoke(非同步,不會阻塞)
    Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
        ' 更新 Label2 顯示開始處理
        Label2.Text = "開始處理(BeginInvoke)..."
        ' 使用 BeginInvoke 執行耗時任務(不會阻塞 UI 執行緒)
        Me.BeginInvoke(New Action(Sub()
                                      ' 模擬耗時操作 3 秒
                                      Thread.Sleep(3000)
                                      ' 使用 BeginInvoke 更新 Label1
                                      Label1.BeginInvoke(Sub()
                                                             ' 更新 Label1 顯示完成訊息
                                                             Label1.Text = "BeginInvoke 執行完成"
                                                         End Sub)
                                  End Sub))
        ' 這行程式碼會立即執行,不會等待 BeginInvoke 完成
        Label2.Text = "BeginInvoke 已啟動(UI 沒有被阻塞)"
    End Sub
End Class
詳細講解

此範例清楚展示了 Invoke 和 BeginInvoke 的差異。當按下 Button1 時,使用 Me.Invoke 執行包含 3 秒延遲的委派。由於 Invoke 是同步執行的,它會阻塞阻塞是指執行緒暫停執行,等待某個操作完成後才繼續。呼叫執行緒(UI 執行緒),直到委派完成。在這 3 秒期間,整個 UI 介面會完全無法響應,使用者無法點擊任何按鈕或控制項。只有當 Invoke 完成後,下一行設定 Label2 的程式碼才會執行。

相比之下,當按下 Button2 時,使用 Me.BeginInvoke 執行相同的委派。BeginInvoke 是非同步執行的,它會立即返回,不會阻塞 UI 執行緒。因此,設定 Label2 的程式碼會立即執行,顯示「BeginInvoke 已啟動(UI 沒有被阻塞)」。在背景任務執行的 3 秒期間,使用者仍然可以與 UI 介面互動,點擊其他按鈕或控制項。

這個對比範例明確展示了:Invoke 適合快速操作且需要立即取得結果的場景,而 BeginInvoke 適合耗時操作且不希望阻塞 UI 的場景。在實際開發中,應該根據具體需求選擇適當的方法。

BeginInvoke 最佳實務建議

建議一:避免在委派中直接訪問 UI 控制項

在背景執行緒的委派中,應該避免直接訪問 UI 控制項。如果必須訪問,使用 Invoke 或 BeginInvoke 來確保在 UI 執行緒上執行。直接訪問可能導致跨執行緒操作錯誤跨執行緒操作錯誤是指在非 UI 執行緒上直接修改 UI 控制項,會拋出 InvalidOperationException 異常。,使程式崩潰。

建議二:適當處理異常

在委派中使用 Try-Catch 區塊來捕獲和處理可能出現的異常。未處理的異常可能導致應用程式崩潰或產生不可預期的行為。使用 BeginInvoke 更新 UI 來顯示錯誤訊息,讓使用者了解發生了什麼問題。

建議三:謹慎使用共享資源

如果多個執行緒需要訪問同一個共享資源,使用適當的同步處理機制(如 SyncLock),以避免競爭條件和資料不一致。BeginInvoke 啟動的執行緒與主執行緒是並行執行的,需要確保執行緒安全執行緒安全是指程式碼能在多執行緒環境下正確執行,不會產生資料競爭或不一致的結果。

注意事項:BeginInvoke 不會自動等待完成

使用 BeginInvoke 後,主執行緒不會等待委派執行完成就繼續執行。如果需要等待結果或確保任務完成,應該使用其他機制(如 IAsyncResult.AsyncWaitHandle)或改用 Invoke。在表單關閉時,背景任務可能仍在執行,需要適當處理以避免資源洩漏或錯誤。