VB.NET 執行緒 筆記(精進篇)
執行緒的本質
就像一個忙碌的工廠裡有許多工人同時進行著不同的工作一樣,執行緒(Thread)是在程式執行時的基本單元,它們像是勤奮的工人,同時執行不同的任務,使整個程式能夠更有效率地運作。每個執行緒都有自己的執行路徑和任務,它們可以並行執行,提高程式的效能和響應能力。
在單執行緒的程式中,任務是按照代碼的順序一步一步地執行。當遇到耗時的操作時(如網絡請求、資料處理等),整個程式就會被阻塞,無法做其他事情,直到該操作完成為止。這就像一個工廠裡只有一名工人在工作,當他專注於一項任務時,其他任務就必須等待。
相比之下,多執行緒程式可以同時執行多個任務,每個執行緒就像一個獨立的工人,專注於自己的工作,互不干擾。當一個執行緒在等待某個操作完成時,其他執行緒可以繼續工作,而不會被阻塞。這樣一來,程式的效能和響應能力都可以得到提升,使用者體驗也會更加流暢。
執行緒(Thread):一個程式內的基本執行單元,它可以並行執行多個任務。每個執行緒都有自己的執行路徑和任務,它們之間可以共享程式的資源(如記憶體、檔案等),但也需要進行適當的同步控制,以避免競爭情況競爭情況是指多個執行緒同時嘗試存取和修改同一個共享資源,導致資源的狀態出現混亂或不一致的情況。。
使用多執行緒可以大大提高程式的效能和響應能力,但同時也需要注意執行緒之間的同步和資源共享問題。適當使用執行緒可以使程式更加高效和靈活,但如果使用不當,也可能導致程式出現錯誤、死鎖或資料損壞等問題。因此,在使用執行緒時,需要仔細考慮並採取適當的同步機制,以確保程式的正確性和穩定性。
執行緒的創建和啟動
創建執行緒的語法
要在 VB.NET 中創建一個新的執行緒,可以使用System.Threading.Thread 類別的構造函數。基本語法如下:
Imports System.Threading
Dim [執行緒變數] As New Thread([委派或子程序])
其中:
[執行緒變數]:用於儲存新建立的執行緒實例。[委派或子程序]:指定執行緒要執行的代碼塊,可以是一個委派委派(Delegate)是一種類似於 C# 中委託(Delegate)的概念,它可以將方法視為參數傳遞。使用委派可以提高程式的靈活性和可擴展性。(Delegate)或子程序(Sub)。
在創建執行緒時,需要指定它要執行的代碼塊。這可以是一個委派委派(Delegate)是一種類似於 C# 中委託(Delegate)的概念,它可以將方法視為參數傳遞。使用委派可以提高程式的靈活性和可擴展性。(Delegate),也可以是一個子程序(Sub)。委派是一種類似於 C# 中委託(Delegate)的概念,它可以將方法視為參數傳遞。使用委派可以提高程式的靈活性和可擴展性。
啟動執行緒的語法
創建好執行緒實例後,需要使用 Start 方法來啟動該執行緒,語法如下:
[執行緒變數].Start()
一旦執行緒被啟動,它會獨立於主執行緒(主程式線程)運行,直到完成為止。需要注意的是,啟動執行緒並不意味著它會立即開始運行,實際的運行時機由操作系統的執行緒調度器執行緒調度器是操作系統的一部分,負責管理和調度執行緒的執行順序和時間。它決定了每個執行緒何時獲得 CPU 時間片來執行任務。決定。
簡單範例
以下是一個簡單的範例,展示了如何在 Windows Form 中創建和啟動一個新的執行緒:
所需元件:
- Button1:用於啟動執行緒的按鈕
- Label1:用於顯示執行緒的輸出結果
Imports System.Threading
Public Class Form1
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
' 創建新的執行緒
Dim myThread As New Thread(AddressOf ThreadProc)
' 啟動執行緒
myThread.Start()
End Sub
Private Sub ThreadProc()
' 執行緒的任務
For i As Integer = 1 To 10
' 注意: 使用 Invoke 方法來避免跨執行緒存取 UI 違例
Label1.Invoke(Sub() Label1.Text &= "執行緒輸出: " & i & Environment.NewLine)
Thread.Sleep(500) ' 模擬耗時操作
Next
End Sub
End Class
在上述範例中,點擊 Button1 後,會創建並啟動一個新的執行緒myThread。該執行緒會執行 ThreadProc 子程序中的代碼,每隔 500 毫秒在 Label1 中輸出一行文字。由於執行緒是獨立運行的,因此主執行緒不會被阻塞,可以繼續響應其他事件。
需要注意的是,由於執行緒在運行時無法直接訪問 UI 控制項,所以使用了Invoke 方法將 UI 操作委派回主執行緒執行,以避免跨執行緒存取 UI 違例跨執行緒存取 UI 違例是指在非 UI 執行緒中直接訪問或修改 UI 控制項,這是不被允許的。必須使用Control.Invoke 或 Control.BeginInvoke方法將 UI 操作委派回 UI 執行緒執行。。
執行緒生命週期
執行緒的生命週期是指執行緒從創建到終止的整個過程,它包括以下幾個狀態:
未啟動狀態(Unstarted)
Dim threadObj As Thread = New Thread(AddressOf ThreadProcedure)
' 執行緒在這個階段處於未啟動狀態
當執行緒對象被創建,但還沒有調用 Start 方法時,執行緒處於未啟動狀態。此時,執行緒只是一個對象實例,還沒有分配任何系統資源。
就緒狀態(Runnable)
threadObj.Start()
' 執行緒進入就緒狀態,等待被執行緒調度器分配 CPU 時間片
當執行緒對象調用 Start 方法後,執行緒進入就緒狀態。此時,執行緒已經具備了運行條件,正在等待執行緒調度器分配 CPU 時間片。一旦獲得時間片,執行緒就可以開始運行。
運行狀態(Running)
' 執行緒獲得 CPU 時間片後進入運行狀態
Private Sub ThreadProcedure()
' 執行緒的代碼塊
End Sub
當執行緒獲得 CPU 時間片後,就進入了運行狀態。此時,執行緒正在執行其代碼塊中的任務。執行緒會一直運行,直到任務完成、時間片用完、被阻塞或被終止。
暫停狀態(Suspended)
threadObj.Suspend() ' 暫停執行緒
' ...
threadObj.Resume() ' 恢復執行緒
執行緒可以通過調用 Suspend 方法暫時暫停運行。處於暫停狀態的執行緒不會獲得 CPU 時間片,也不會執行任何任務。可以通過調用Resume 方法恢復執行緒的運行。
阻塞狀態(Blocked)
Private Sub ThreadProcedure()
SyncLock lockObject ' 執行緒進入阻塞狀態,等待獲取鎖
' ...
End SyncLock
End Sub
當執行緒因為某些原因(如等待鎖、等待 I/O 操作完成等)而無法繼續運行時,就進入了阻塞狀態。處於阻塞狀態的執行緒會暫時釋放 CPU,讓其他執行緒有機會運行。一旦阻塞條件解除,執行緒就可以重新進入就緒狀態,等待獲得 CPU 時間片。
終止狀態(Terminated)
Private Sub ThreadProcedure()
' ...
' 執行緒完成任務後自動進入終止狀態
End Sub
' 或者手動終止執行緒
threadObj.Abort() ' 不推薦使用,可能導致資源洩漏
當執行緒完成了它的任務,或者被顯式終止時,就進入了終止狀態。處於終止狀態的執行緒已經停止運行,不會再獲得 CPU 時間片。執行緒在終止後會釋放所有分配給它的系統資源。
需要注意的是,雖然可以通過調用 Abort 方法強制終止執行緒,但這種做法是不推薦的。強制終止執行緒可能會導致資源洩漏和其他不可預期的問題。更好的做法是讓執行緒自然結束,或者使用協作取消機制(如CancellationToken)來安全地終止執行緒。
理解執行緒的生命週期對於正確使用和管理執行緒非常重要。不同狀態的轉換反映了執行緒的運行過程,也影響著程式的行為和效能。在設計多執行緒程式時,需要仔細考慮執行緒的狀態變化,以確保程式的正確性和穩定性。
執行緒優先級
執行緒優先級是一個整數值,用於指示執行緒相對於其他執行緒的重要性。優先級較高的執行緒通常會獲得更多的 CPU 時間片,因此可以更快地完成任務。而優先級較低的執行緒則可能需要等待更長的時間才能獲得 CPU 資源。
在 VB.NET 中,可以使用 Thread.Priority 屬性來設置和獲取執行緒的優先級。優先級是一個 ThreadPriority 枚舉值,包括以下幾個級別:
Dim myThread As New Thread(AddressOf ThreadProc)
myThread.Priority = ThreadPriority.Highest ' 設置執行緒優先級為最高
myThread.Start()
Private Sub ThreadProc() ' ...
End Sub
可用的執行緒優先級包括:
Highest:最高優先級,執行緒會優先獲得 CPU 時間片。AboveNormal:高於正常優先級,執行緒會比正常優先級的執行緒更頻繁地獲得 CPU 時間片。Normal:正常優先級,這是執行緒的默認優先級。BelowNormal:低於正常優先級,執行緒會比正常優先級的執行緒更少地獲得 CPU 時間片。Lowest:最低優先級,執行緒只有在其他更高優先級的執行緒都不需要 CPU 時才能獲得時間片。
需要注意的是,盡管可以設置執行緒的優先級,但實際的執行順序和時間片分配還受到操作系統執行緒調度器執行緒調度器是操作系統的一部分,負責管理和調度執行緒的執行順序和時間。它決定了每個執行緒何時獲得 CPU 時間片來執行任務。的控制。不同的操作系統可能有不同的調度策略,因此執行緒的實際運行情況可能與預期有所不同。
此外,過度依賴執行緒優先級可能會導致一些問題,如執行緒飢餓執行緒飢餓是指一個執行緒因為優先級較低而長時間得不到 CPU 時間片,導致任務遲遲無法完成。這通常發生在高優先級執行緒長時間佔用 CPU 的情況下。和優先級反轉等。因此,在大多數情況下,建議讓執行緒保持默認的 Normal 優先級,而不是過度調整優先級。只有在確實需要控制執行緒的相對重要性時,才考慮使用優先級。
執行緒優先級提供了一種影響執行緒調度的方式,但它並不能完全控制執行緒的運行順序和時間。在使用執行緒優先級時,需要謹慎,並充分考慮可能帶來的影響。更重要的是設計合理的多執行緒程式結構,並使用適當的同步機制來協調執行緒之間的互動。
執行緒同步
在多執行緒程式中,執行緒之間需要協調它們對共享資源的訪問,以避免競爭情況競爭情況是指多個執行緒同時嘗試存取和修改同一個共享資源,導致資源的狀態出現混亂或不一致的情況。和其他同步問題。執行緒同步是一種機制,用於控制執行緒之間的互動,確保它們以一種協調且可預期的方式訪問共享資源。
VB.NET 提供了多種執行緒同步的機制和技術,下面介紹幾種常用的方法:
鎖定 (Lock) 語句
鎖定語句是一種簡單且常用的同步機制,用於保護對共享資源的訪問。透過鎖定語句,可以確保同一時間只有一個執行緒能夠進入被鎖定的代碼塊,從而避免競爭情況。在 VB.NET中,可以使用 SyncLock 語句來實現鎖定:
Private sharedValue As Integer = 0
Private lockObject As New Object()
Private Sub UpdateSharedValue()
' 模擬耗時操作
Thread.Sleep(500)
' 使用 SyncLock 保護對共享變數的存取
SyncLock lockObject
sharedValue += 1
End SyncLock
End Sub
在上面的範例中,使用 SyncLock 語句來保護對sharedValue 共享變數的存取。通過鎖定lockObject,確保同一時間只有一個執行緒能夠進入SyncLock 塊內部,從而避免了競爭情況。
需要注意的是,鎖定對象(如 lockObject)必須是引用類型,且應該是一個專門用於鎖定的私有實例,以避免不必要的鎖定競爭和死鎖問題。
其他同步機制
除了鎖定語句外,VB.NET 還提供了其他一些同步機制,如:
Monitor類別:提供了更細粒度的鎖定控制,如Enter、Exit、Wait和Pulse等方法。Interlocked類別:提供了一組原子操作原子操作是指不可分割的操作,即要麼完全執行,要麼完全不執行。在多執行緒環境下,原子操作可以確保操作的完整性,避免競爭情況。,用於對共享變數進行線程安全的讀取、寫入和修改。- 信號量(
Semaphore):用於控制同時訪問某個資源或執行某個操作的執行緒數量。 - 事件(
Event):用於在執行緒之間發送信號,以協調它們的操作。 - 讀寫鎖(
ReaderWriterLockSlim):提供了更細粒度的鎖定控制,允許多個執行緒同時讀取資源,但只允許一個執行緒寫入資源。
這些同步機制各有其特點和適用場景,可以根據具體的需求來選擇使用。在使用這些機制時,需要注意死鎖、活鎖活鎖是指兩個或多個執行緒因為不斷重試而無法繼續執行的情況。與死鎖不同,活鎖中的執行緒並未被阻塞,而是不斷嘗試執行,但由於某些條件無法滿足,導致任務無法進展。和其他同步問題,編寫線程安全的代碼。
總的來說,執行緒同步是多執行緒程式設計中的重要概念,它確保了執行緒之間的協調和資源的安全訪問。通過使用適當的同步機制,如鎖定語句、事件、信號量等,可以有效地防止競爭情況和其他同步問題,從而提高程式的正確性和穩定性。
執行緒池
執行緒池是一種管理和復用執行緒的機制,它維護了一組預先創建的執行緒,這些執行緒可以被重複使用來執行多個任務,而無需為每個任務創建新的執行緒。使用執行緒池可以提高程式的效能和資源利用率,尤其是在需要頻繁創建和銷毀執行緒的場景下。
在 VB.NET 中,可以使用 System.Threading.ThreadPool 類別來訪問和管理執行緒池。以下是使用執行緒池的一些常見方法:
範例:使用執行緒池執行工作項
Imports System.Threading
Public Class Form1
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
' 將工作項加入執行緒池
ThreadPool.QueueUserWorkItem(AddressOf WorkItemProc)
End Sub
Private Sub WorkItemProc(state As Object)
' 執行工作項的任務
For i As Integer = 1 To 10
' 注意: 使用 Invoke 方法來避免跨執行緒存取 UI 違例
ListBox1.Invoke(Sub() ListBox1.Items.Add("工作項輸出: " & i))
Thread.Sleep(500)
Next
End Sub
End Class
在上面的範例中,當點擊 Button1 時,使用ThreadPool.QueueUserWorkItem 方法將一個工作項加入執行緒池中。工作項是由 WorkItemProc 子程序定義的,它會執行一些任務(如在 ListBox1 中添加項目)。
執行緒池會自動管理工作項的執行,根據可用的執行緒數量和系統負載情況,動態地分配執行緒來執行工作項。這樣可以避免為每個任務創建新的執行緒所帶來的開銷,提高了程式的效能。
需要注意的是,由於工作項是在執行緒池的執行緒上運行的,因此訪問 UI 控制項時仍然需要使用 Invoke 或 BeginInvoke 方法將操作委派回 UI 執行緒,以避免跨執行緒存取 UI 的違例。
執行緒池的限制:
- 無法精確控制工作項的執行順序,工作項是按照排隊的順序執行的,但實際執行順序可能會受到執行緒調度等因素的影響。
- 無法直接獲取工作項的返回值,因為工作項是在執行緒池的執行緒上運行的,無法直接返回結果給調用方。如果需要返回值,可以使用異步編程模型如
Task。 - 工作項中的異常處理較為困難,因為異常是在執行緒池的執行緒上拋出的,無法直接在調用方的上下文中捕獲和處理。需要在工作項內部進行異常處理。
- 執行緒池的執行緒數量是由系統自動管理的,無法直接控制執行緒的創建和銷毀。對於需要精確控制執行緒生命週期的場景,可能需要手動創建和管理執行緒。
儘管存在這些限制,但對於許多場景而言,使用執行緒池仍然是一種方便且高效的多執行緒編程方式。它可以簡化程式的實現,提高資源利用率,並自動管理執行緒的生命週期。在選擇使用執行緒池時,需要權衡其優缺點,並根據具體的需求進行設計和實現。
異步編程
異步編程是一種允許程式在執行長時間運行的操作時繼續響應用戶互動的編程模型。它通過將耗時的操作(如 I/O 操作、網絡請求等)放在後台執行,而不阻塞主執行緒,從而提高了程式的響應能力和性能。
在 VB.NET 中,可以使用基於 Task 的異步編程模型(TAP)來實現異步操作。TAP 使用 Async 和 Await 關鍵字,以及Task 和 Task(Of T) 類型,提供了一種簡潔且易於使用的異步編程方式。
使用 Task 實現異步操作
Imports System.Threading.Tasks
Public Class Form1
Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
' 調用異步方法
Dim result As String = Await PerformLongRunningOperationAsync()
' 顯示結果
Label1.Text = result
End Sub
Private Async Function PerformLongRunningOperationAsync() As Task(Of String)
' 模擬一個耗時操作
Await Task.Delay(5000)
' 返回操作結果
Return "長時間運行的操作已完成"
End Function
End Class
在上面的範例中,當點擊 Button1 時,調用了一個異步方法PerformLongRunningOperationAsync。該方法使用Async 關鍵字標記,表示它是一個異步方法,並返回一個Task(Of String) 類型的任務。
在異步方法內部,使用 Await 關鍵字來等待一個異步操作完成。在這個範例中,使用Task.Delay 方法模擬了一個耗時 5 秒的操作。當異步操作完成後,方法會返回一個字符串結果。
在調用異步方法時,同樣使用 Await 關鍵字來等待異步操作完成,並將結果賦值給 result 變數。最後,將結果顯示在 Label1 上。
使用 Async 和 Await 關鍵字可以讓異步代碼的編寫和閱讀變得更加簡潔和直觀。它們處理了異步操作完成時的回調和狀態管理,使得異步代碼的編寫方式與同步代碼類似。
使用 async 和 await 簡化異步編程
使用 Async 和 Await 關鍵字可以大大簡化異步編程的代碼。與傳統的基於事件和回調的異步編程模式相比,基於 Task 的異步模式(TAP)具有以下優勢:
- 代碼更加簡潔和易讀,避免了深層嵌套的回調函數(回調地獄回調地獄是指在使用傳統的異步編程模式時,因為需要處理大量的異步操作和回調函數,導致代碼變得深層嵌套、難以理解和維護的情況。)。
- 異步代碼的編寫方式與同步代碼類似,減少了編寫和理解異步代碼的難度。
- 支持使用
Try...Catch...Finally塊來處理異常,使得異常處理更加簡單和一致。 - 可以使用
Await關鍵字來等待多個異步操作完成,並且可以在異步方法中返回值。
異步編程的限制和注意事項:
- 異步編程增加了代碼的複雜性,特別是在處理異常、取消操作和同步上下文時,需要額外的注意。
- 不當地使用
Await關鍵字可能導致性能問題,如果在快速完成的操作上使用Await,反而會增加額外的開銷。 - 異步方法的調用方式與同步方法不同,需要使用
Await關鍵字來等待異步操作完成,這可能影響代碼的組織和調用方式。 - 在使用一些較舊的 APIs 或庫時,可能沒有提供異步版本,需要進行封裝或使用其他技術來實現異步調用。
- 異步代碼的調試和測試可能更加複雜,需要使用專門的調試技術和工具來診斷問題。
儘管異步編程存在一些限制和注意事項,但對於 I/O 密集型的操作,如文件訪問、網絡通信等,使用異步編程可以顯著提高程式的響應能力和性能。在選擇是否使用異步編程時,需要綜合考慮程式的需求、複雜性和可維護性等因素。
取消異步操作
在某些情況下,可能需要取消正在進行的異步操作,例如當用戶點擊取消按鈕或者某個條件不再滿足時。.NET 提供了 CancellationToken 機制來支持異步操作的取消。
Imports System.Threading
Imports System.Threading.Tasks
Public Class Form1
Private cts As CancellationTokenSource
Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
cts = New CancellationTokenSource()
Try
' 調用異步方法並傳入取消標記
Dim result As String = Await PerformLongRunningOperationAsync(cts.Token)
' 顯示結果
Label1.Text = result
Catch ex As OperationCanceledException
' 異步操作被取消時的處理
Label1.Text = "操作已被取消"
End Try
End Sub
Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
' 取消異步操作
cts.Cancel()
End Sub
Private Async Function PerformLongRunningOperationAsync(cancellationToken As CancellationToken) As Task(Of String)
' 模擬一個耗時操作,並檢查取消標記
For i As Integer = 1 To 10
If cancellationToken.IsCancellationRequested Then
' 操作被取消,拋出 OperationCanceledException
cancellationToken.ThrowIfCancellationRequested()
End If
Await Task.Delay(500)
Next
' 返回操作結果
Return "長時間運行的操作已完成"
End Function
End Class
在上面的範例中,創建了一個 CancellationTokenSource 對象 cts,並將其 Token 屬性傳遞給異步方法 PerformLongRunningOperationAsync。在異步方法內部,使用cancellationToken.IsCancellationRequested 屬性來檢查操作是否被取消,如果被取消,則通過調用cancellationToken.ThrowIfCancellationRequested() 方法拋出OperationCanceledException 異常。
在調用異步方法的代碼中,將 cts.Token 傳遞給異步方法,並使用Try...Catch 塊來捕獲可能拋出的OperationCanceledException 異常,以進行相應的處理。
當需要取消異步操作時,可以調用 cts.Cancel() 方法,該方法會將取消請求發送給所有使用該 Token 的異步操作。異步操作收到取消請求後,會拋出OperationCanceledException 異常,並終止執行。
使用 CancellationToken 機制可以使異步操作支持取消功能,提高程式的靈活性和用戶體驗。在編寫異步代碼時,建議考慮是否需要支持取消操作,並在適當的地方檢查取消標記,以便及時響應取消請求。
執行緒安全性
在多執行緒程式中,多個執行緒可能同時訪問和修改共享的資源,如果沒有適當的同步機制,就可能導致資源的狀態出現競爭情況競爭情況是指多個執行緒同時嘗試存取和修改同一個共享資源,導致資源的狀態出現混亂或不一致的情況。和不一致。執行緒安全性是指程式在多執行緒環境下能夠正確地運行,避免因並發訪問而導致的錯誤和不可預期的行為。
為了實現執行緒安全性,可以採用以下幾種常見的技術:
- 鎖定(Locking)
- 執行緒本地存儲(Thread-Local Storage,TLS)
- 不可變對象(Immutable Objects)
- 原子操作(Atomic Operations)
Private sharedData As New Object()
Private threadSafeObject As New ThreadSafeObject()
Public Sub AccessSharedData()
SyncLock sharedData
' 存取共享資源
End SyncLock
End Sub
使用鎖定機制(如 SyncLock 語句)來保護對共享資源的訪問。鎖定確保同一時間只有一個執行緒能夠進入被鎖定的代碼塊,避免了競爭情況競爭情況是指多個執行緒同時嘗試存取和修改同一個共享資源,導致資源的狀態出現混亂或不一致的情況。。
[ThreadStatic]
Private Shared threadData As Integer
Public Sub UpdateThreadData()
threadData += 1
End Sub
使用執行緒本地存儲(TLS)為每個執行緒分配獨立的資源副本,避免了執行緒之間的共享和競爭。通過 [ThreadStatic] 屬性,可以將靜態字段標記為執行緒本地的。
Public NotInheritable Class ImmutableData
Private ReadOnly _value As Integer
Public Sub New(value As Integer)
_value = value
End Sub
Public ReadOnly Property Value As Integer
Get
Return _value
End Get
End Property
End Class
使用不可變對象,一旦對象被創建,其狀態就不能被修改。不可變對象在多執行緒環境下是安全的,因為它們的狀態不會受到並發訪問的影響。
Private shared _count As Integer
Public Sub IncrementCount()
Interlocked.Increment(_count)
End Sub
使用原子操作(如 Interlocked 類提供的方法)來對共享變數進行原子級別的讀取、寫入和修改操作。原子操作能夠確保在執行期間不會被中斷,從而避免了競爭情況競爭情況是指多個執行緒同時嘗試存取和修改同一個共享資源,導致資源的狀態出現混亂或不一致的情況。。
除了上述技術外,還可以通過適當的設計來提高執行緒安全性,例如:
- 盡可能減少共享狀態的範圍和生命週期。
- 謹慎使用靜態數據成員,因為它們會被所有執行緒共享。
- 在設計階段就考慮併發問題,而不是事後才進行修復。
- 充分測試和驗證併發代碼,因為並發錯誤往往是間歇性的、難以重現的。
執行緒安全性的最佳選擇
' 始終保護對共享資源的存取
Private sharedData As New Object()
Private threadSafeObject As New ThreadSafeObject()
Public Sub AccessSharedData()
SyncLock sharedData
' 存取共享資源
End SyncLock
End Sub
Public Sub AccessThreadSafeObject()
' 訪問執行緒安全的對象無需同步
Dim data = threadSafeObject.GetData()
End Sub
' 盡可能使用執行緒安全的集合
Private threadSafeDictionary As New ConcurrentDictionary(Of String, Integer)
' 避免靜態數據成員
Private Class ThreadSafeObject
Private _data As Integer
Public Function GetData() As Integer
Return _data
End Function
Public Sub SetData(value As Integer)
_data = value
End Sub
End Class
' 使用更細粒度的同步機制
Private readWriteLock As New ReaderWriterLockSlim()
Public Function GetSharedData() As Integer
readWriteLock.EnterReadLock()
Try
' 讀取共享數據
Finally
readWriteLock.ExitReadLock()
End Try
End Function
Public Sub UpdateSharedData(newValue As Integer)
readWriteLock.EnterWriteLock()
Try
' 更新共享數據
Finally
readWriteLock.ExitWriteLock()
End Try
End Sub
需要保持警惕,因為並發編程本身就是一個複雜的領域,任何疏忽都可能導致嚴重的後果。
此外,在設計和實現多執行緒系統時,還需要考慮一些其他因素,例如:
- 死鎖預防: 採用適當的鎖定順序,避免發生死鎖。
- 飢餓預防: 確保每個執行緒都有機會獲得所需的資源,避免某些執行緒長時間得不到執行。
- 執行緒安全設計: 在設計階段就考慮執行緒安全性,選擇合適的並發模型和同步機制。
- 測試和驗證: 針對並發代碼進行充分的測試和驗證,包括極端情況和邊界條件的測試。
通過綜合運用各種技術開發出可靠、健壯的多執行緒應用程式,充分發揮多核 CPU的性能外,同時也需要謹慎對待,因為執行緒安全性問題往往難以發現和重現,需要保持警惕和謹慎的態度。
執行緒的限制和注意事項
盡管執行緒可以大大提高程式的效能和響應能力,但也存在一些限制和需要注意的地方:
- 執行緒是一種有限的資源:系統中可以創建的執行緒數量是有限的,過多的執行緒會導致系統資源的過度消耗,反而降低程式的性能。因此,在創建執行緒時需要合理控制執行緒的數量。
- 執行緒之間的通信和同步很複雜:當多個執行緒需要訪問共享資源時,就需要進行適當的通信和同步,以避免競爭情況競爭情況是指多個執行緒同時嘗試存取和修改同一個共享資源,導致資源的狀態出現混亂或不一致的情況。。這增加了程式的複雜性,也容易引入錯誤。
- 執行緒調度是不確定的:執行緒的調度由操作系統完成,程式無法控制執行緒的具體調度順序。這可能會導致一些潛在的問題,例如執行緒飢餓執行緒飢餓是指一個執行緒因為優先級較低或者總是得不到所需資源,而長時間得不到執行的情況。這可能導致程式的響應能力下降或者某些任務無法完成。。
- 執行緒可能會導致死鎖死鎖是指兩個或多個執行緒因為互相等待對方持有的資源而陷入永久阻塞的情況。當執行緒 A 持有資源 X,並等待執行緒 B 釋放資源 Y,而執行緒 B 持有資源 Y,並等待執行緒 A 釋放資源 X 時,就會發生死鎖。:如果多個執行緒互相等待對方釋放資源,就會發生死鎖情況,導致程式無法繼續執行。
- 執行緒對於某些操作可能是無效的:某些操作可能不是執行緒安全的,例如對 UI 控制項的訪問。在這種情況下,需要採取特殊的措施,例如使用控件的
Invoke方法將操作委派回 UI 執行緒。
為了有效地利用執行緒並避免潛在的問題,建議參照以下事項:
- 合理控制執行緒的數量,不要過度創建執行緒。
- 適當地使用同步機制來保護共享資源的訪問。
- 盡可能減少執行緒之間的通信和共享狀態。
- 充分測試和驗證多執行緒代碼,特別是在各種異常情況下。
- 對於耗時的操作,考慮使用異步編程模型而不是創建新的執行緒。
- 遵循執行緒安全的編程 best practicesbest practices 是指在特定領域或活動中被廣泛認可和接受的最佳實踐方式或指導原則,通常代表著最有效、最高效的工作方法。遵循 best practices 有助於提高代碼質量、可維護性和性能。,例如避免死鎖、活鎖活鎖是指兩個或多個執行緒因為不斷重試而無法繼續執行的情況。與死鎖不同,活鎖中的執行緒並未被阻塞,而是不斷嘗試執行,但由於某些條件無法滿足,導致任務無法進展。等問題。
執行緒是一把雙刃劍。合理使用執行緒可以顯著提高程式的效能和響應能力,但使用不當也會帶來嚴重的問題。只有在深入理解執行緒的特性和局限性的基礎上,才能編寫出可靠的多執行緒程式。
進階主題
在前面的章節中,介紹了執行緒的基本概念、執行緒的創建和啟動、執行緒同步、執行緒池、異步編程和執行緒安全性等內容。接下來,讓我們探討一些更加進階的主題,以便更好地理解和運用執行緒。
執行緒池的管理和優化
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
' 獲取可用的工作者執行緒數量
Dim workerThreads, completionPortThreads As Integer
ThreadPool.GetAvailableThreads(workerThreads, completionPortThreads)
' 設置執行緒池的最大工作者執行緒數量
ThreadPool.SetMaxThreads(100, completionPortThreads)
' 將多個工作項加入執行緒池
For i As Integer = 1 To 50
ThreadPool.QueueUserWorkItem(AddressOf WorkItemProc)
Next
End Sub
Private Sub WorkItemProc(state As Object)
' 執行工作項任務
...
End Sub
執行緒池雖然方便且高效,但也需要進行適當的管理和優化。可以通過ThreadPool.GetAvailableThreads 方法獲取當前可用的工作者執行緒數量,並使用 ThreadPool.SetMaxThreads 方法設置執行緒池的最大工作者執行緒數量,以控制執行緒池的規模。
此外,還可以通過監控執行緒池的性能指標,如執行緒池的使用情況、工作項的排隊長度等,來評估執行緒池的運行狀態,並根據需要進行調整。對於一些長時間運行的工作項,可以考慮使用專門的執行緒而不是執行緒池,以避免佔用過多的執行緒池資源。
執行緒本地存儲 (Thread Local Storage)
[ThreadStatic]
Private Shared localData As Integer
Public Sub IncrementLocalData()
localData += 1
End Sub
執行緒本地存儲 (Thread Local Storage, TLS) 允許每個執行緒擁有變數的獨立副本。通過將變數標記為 [ThreadStatic],每個執行緒都會擁有該變數的私有實例,互不干擾。這對於需要在執行緒內部維護狀態或上下文的場景非常有用。
不過需要注意的是,由於每個執行緒都有變數的獨立副本,因此 TLS 可能會增加記憶體的消耗。此外,TLS 中的變數在執行緒結束時不會自動釋放,需要顯式地管理它們的生命週期,以避免記憶體洩漏。
執行緒同步的進階技術
' 使用 Monitor 進行更細粒度的鎖定控制
Private lockObject As New Object()
Public Sub CriticalSection()
Monitor.Enter(lockObject)
Try
' 臨界區代碼
Finally
Monitor.Exit(lockObject)
End Try
End Sub
' 使用 ReaderWriterLockSlim 實現讀寫鎖
Private rwLock As New ReaderWriterLockSlim()
Public Sub ReaderFunction()
rwLock.EnterReadLock()
Try
' 讀取共享資源
Finally
rwLock.ExitReadLock()
End Try
End Sub
Public Sub WriterFunction()
rwLock.EnterWriteLock()
Try
' 寫入共享資源
Finally
rwLock.ExitWriteLock()
End Try
End Sub
除了基本的鎖定和同步機制外,還有一些更加進階的技術可以用於實現執行緒同步。例如,Monitor 類提供了更細粒度的鎖定控制,可以使用Enter、Exit、Wait 和Pulse 等方法來實現更複雜的同步邏輯。
另一個常用的技術是讀寫鎖,如 ReaderWriterLockSlim 類。讀寫鎖允許多個執行緒同時讀取共享資源,但寫入資源時需要獨占鎖。這對於讀多寫少的場景可以顯著提高並發性能。
除此之外,還有其他一些同步技術,如信號量、事件、條件變數等,它們各自有其特定的使用場景和優缺點。選擇合適的同步技術需要根據具體的需求和性能要求進行權衡。
執行緒和任務的取消和超時
Private cancellationTokenSource As New CancellationTokenSource()
Private Async Sub StartButton_Click(sender As Object, e As EventArgs) Handles StartButton.Click
Try
' 傳遞取消標記給任務
Await Task.Run(AddressOf LongRunningTask, cancellationTokenSource.Token)
MessageBox.Show("任務完成")
Catch ex As OperationCanceledException
MessageBox.Show("任務已取消")
End Try
End Sub
Private Async Sub CancelButton_Click(sender As Object, e As EventArgs) Handles CancelButton.Click
' 請求取消任務
cancellationTokenSource.Cancel()
End Sub
Private Sub LongRunningTask(cancellationToken As CancellationToken)
For i As Integer = 1 To 100
' 檢查取消請求
cancellationToken.ThrowIfCancellationRequested()
' 模擬耗時操作
Thread.Sleep(100)
Next
End Sub
在執行長時間運行的任務時,通常需要支持取消操作,以便在必要時中斷任務的執行。CancellationToken 和CancellationTokenSource 類提供了一種協作式的取消機制,可以在任務中檢查取消請求,並在取消時拋出OperationCanceledException 異常。
除了取消之外,還可以為任務設置超時時間,以防止任務無限期地運行。可以使用CancellationTokenSource.CancelAfter 方法在指定時間後自動取消任務,或者使用 Task.Wait 和 Task.WaitAny 等方法指定超時時間。
在設計和實現取消和超時機制時,需要仔細考慮任務的可取消性和對取消的響應方式。並非所有的任務都能夠在任意時刻安全地取消,有些任務可能需要執行一些清理或回滾操作才能夠正常終止。因此,需要根據具體的業務需求來設計合適的取消策略。
提示和技巧
在使用執行緒進行多執行緒編程時,以下是一些值得注意的提示和技巧:
儘量避免創建過多的執行緒
' 使用執行緒池來重用執行緒
ThreadPool.QueueUserWorkItem(AddressOf WorkerThread)
' 使用 Task 和異步編程模型
Dim result = Await Task.Run(Function() LongRunningOperation())
創建和銷毀執行緒是一個相對昂貴的操作。過多的執行緒不僅會消耗系統資源,還可能導致過多的上下文切換,反而降低程式的性能。因此,建議儘量避免為每個任務創建單獨的執行緒,而是使用執行緒池或者基於任務的異步編程模型來重用和管理執行緒。
謹慎使用鎖定和同步
' 儘量減小鎖定的範圍
SyncLock lockObject
' 只對需要同步的代碼部分進行鎖定
End SyncLock
' 避免在鎖定期間執行耗時操作
SyncLock lockObject
' 不要在鎖定期間進行 I/O 操作或者調用耗時方法
End SyncLock
雖然鎖定和同步是保護共享資源的必要手段,但過度或不當的使用鎖定也可能導致性能問題和死鎖風險。因此,在使用鎖定時需要特別謹慎,儘量減小鎖定的範圍,只對需要同步的代碼部分進行鎖定,避免在鎖定期間執行耗時操作,以減少鎖定的持有時間。
此外,還需要注意鎖定的粒度和順序,避免出現死鎖情況。可以考慮使用更高級的同步機制,如讀寫鎖、信號量等,來實現更細粒度的同步控制。
優先使用基於任務的異步編程模型
' 使用 Async 和 Await 關鍵字簡化異步代碼
Private Async Function DownloadDataAsync() As Task(Of String)
Using clientAs New HttpClient()
Return Await client.GetStringAsync("http://example.com/data")
End Using
End Function
' 在事件處理程式中調用異步方法
Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim data = Await DownloadDataAsync()
' 處理下載的數據
End Sub
對於 I/O 密集型的操作,如文件讀寫、網絡通信等,使用基於任務的異步編程模型可以顯著提高程式的響應能力和性能。通過 Async 和Await 關鍵字,可以以同步的方式編寫異步代碼,使代碼更加簡潔和易於理解。
在使用異步編程時,需要注意避免過度的並行和不必要的等待。對於計算密集型的操作,使用異步編程可能反而會降低性能。因此,需要根據具體的場景選擇合適的編程模型。
盡量減少共享狀態和數據競爭
' 使用本地變數而不是共享變數
Private Sub ThreadProc(number As Integer)
Dim localResult As Integer = 0
' 使用本地變數 localResult 進行計算
' ...
' 在需要時再同步更新共享變數
SyncLock lockObject
sharedResult += localResult
End SyncLock
End Sub
共享狀態和數據競爭是多執行緒程式中常見的問題來源。為了降低這些問題的風險,可以儘量減少共享狀態的使用,將共享數據的訪問限制在必要的範圍內。可以考慮使用執行緒本地存儲(TLS)或者將共享數據封裝在線程安全的類中。
在需要訪問共享數據時,應該使用適當的同步機制來保護數據的一致性。盡量使用本地變數來存儲中間結果,只在需要時才進行共享數據的同步更新。
適當地管理執行緒的生命週期
' 等待執行緒完成
Dim thread As New Thread(AddressOf ThreadProc)
thread.Start()
' 執行其他操作
' ...
thread.Join() ' 等待執行緒完成
' 使用 CancellationToken 取消執行緒或任務
Dim cancellationToken As CancellationToken = cancellationTokenSource.Token
Dim task As Task = Task.Run(Sub()
' 執行任務
' 定期檢查取消請求
If cancellationToken.IsCancellationRequested Then
' 處理取消操作
Return
End If
End Sub, cancellationToken)
在創建執行緒或任務後,需要適當地管理它們的生命週期。對於需要等待執行緒完成的情況,可以使用 Thread.Join 方法來阻塞當前執行緒,直到目標執行緒完成。
對於長時間運行的執行緒或任務,可以考慮提供取消機制,以便在需要時安全地終止它們的執行。可以使用 CancellationToken 來傳遞取消請求,並在執行緒或任務中定期檢查取消狀態。
在執行緒或任務完成後,需要適當地釋放它們佔用的資源,如取消註冊的事件、關閉打開的文件或連接等。這有助於避免資源洩漏和提高程式的穩定性。
善用並行集合和線程安全類型
' 使用 ConcurrentDictionary 代替 Dictionary
Dim dictionary As New ConcurrentDictionary(Of String, Integer)()
' 使用 ConcurrentBag 存儲共享的資料
Dim bag As New ConcurrentBag(Of String)()
' 使用 BlockingCollection 實現生產者-消費者模式
Dim buffer As New BlockingCollection(Of Integer)()
' 生產者執行緒
Dim producerThread As New Thread(Sub()
For i As Integer = 1 To 10
buffer.Add(i)
Thread.Sleep(100)
Next
buffer.CompleteAdding()
End Sub)
producerThread.Start()
' 消費者執行緒
Dim consumerThread As New Thread(Sub()
While True
Try
Dim item As Integer = buffer.Take()
' 處理獲取的資料項
Console.WriteLine("Consumed: " & item)
Catch ex As InvalidOperationException
' 集合已完成添加,退出迴圈
Exit While
End Try
End While
End Sub)
consumerThread.Start()
.NET 提供了一些並行集合和線程安全類型,如ConcurrentDictionary、ConcurrentBag、BlockingCollection 等,這些類型在多執行緒環境下是安全的,可以顯著簡化程式設計。
例如,使用 ConcurrentDictionary 可以避免在訪問字典時手動進行鎖定,使用 ConcurrentBag 可以方便地存儲和訪問共享的資料集合。BlockingCollection 則提供了一種易用的方式來實現生產者-消費者模式,自動處理執行緒之間的同步和通信。
盡可能使用這些並行集合和線程安全類型,可以減少手動編寫同步代碼的需要,提高程式的正確性和可維護性。