文章目錄
- 1. 線程管理
- 1.1 線程的核心概念:System.Threading.Thread
- 1.2 現代線程管理:System.Threading.Tasks.Task 和 Task Parallel Library (TPL)
- 1.3 狀態管理和異常處理
- 1.4 協調任務:async/await 模式
- 2. 線程間通信
- 2.1 共享內存與競態條件
- 2.2 同步原語:確保線程安全
- 2.3 線程安全的數據結構
- 2. 多線程編程模式與最佳實踐
- 2.1 模式
- 2.1 最佳實踐與常見陷阱
- 2.2 補充
1. 線程管理
.NET 的線程管理建立在操作系統線程之上,但提供了更高級別的抽象和更豐富的功能來簡化并發編程。
1.1 線程的核心概念:System.Threading.Thread
這是最基礎的線程類,直接包裝了操作系統線程。
using System.Threading;// 創建并啟動一個新線程
Thread newThread = new Thread(WorkerMethod);
newThread.Start(); // 開始執行 WorkerMethodvoid WorkerMethod()
{Console.WriteLine($"我在另一個線程上運行!線程ID: {Thread.CurrentThread.ManagedThreadId}");
}
- 前臺線程 vs. 后臺線程:
- 前臺線程:默認創建的線程是前臺線程。只要有一個前臺線程還在運行,應用程序進程就不會終止。
- 后臺線程:通過 IsBackground = true 設置。當所有前臺線程結束時,CLR 會強制終止所有后臺線程,無論其是否執行完畢。適用于非關鍵任務(如心跳檢測、日志刷新)。
Thread bgThread = new Thread(WorkerMethod);
bgThread.IsBackground = true;
bgThread.Start();
- 線程狀態:通過 ThreadState 枚舉表示(Unstarted, Running, WaitSleepJoin, Stopped 等)。
- 線程池:直接創建和銷毀線程開銷很大。.NET 提供了一個線程池來管理一組重用的工作線程。
1.2 現代線程管理:System.Threading.Tasks.Task 和 Task Parallel Library (TPL)
從 .NET 4.0 開始,Task 成為了推薦的多線程和異步編程模型。它是對 Thread 的高級封裝,極大地簡化了復雜操作。
- 什么是 Task: 表示一個異步操作。它不一定映射到獨占的操作系統線程。它可能在線程池線程上運行,也可能使用 I/O 完成端口等機制,效率更高。
- 線程池: Task 默認使用線程池中的線程。線程池會智能地管理線程數量,根據系統負載創建和銷毀線程,避免了頻繁創建新線程的開銷。
- 創建和啟動 Task:
// 方式一:Task.Run (最常用,用于將工作排到線程池)
Task task = Task.Run(() =>
{Console.WriteLine($"Task 在線程池線程上運行。線程ID: {Thread.CurrentThread.ManagedThreadId}");// 模擬工作Thread.Sleep(1000);
});// 方式二:Task.Factory.StartNew (提供更多選項)
Task task2 = Task.Factory.StartNew(() => { /* ... */ }, TaskCreationOptions.LongRunning); // 提示線程池這可能是個長任務// 等待任務完成
task.Wait(); // 阻塞當前線程,直到 task 完成
1.3 狀態管理和異常處理
- 狀態查詢: Task.Status 屬性(Created, WaitingToRun, Running, RanToCompletion, Faulted, Canceled)。
- 返回值: Task 可以返回值。
Task<int> calculationTask = Task.Run(() => CalculateSomething());
int result = calculationTask.Result; // 獲取結果(如果任務未完成,會阻塞當前線程)
- 異常處理: Task 中的異常會被捕獲并存儲在 Task.Exception 屬性中(一個 AggregateException)。當你調用 .Wait(), .Result, 或 .WaitAll() 時,這些異常會被重新拋出。
try
{task.Wait(); // 或者訪問 task.Result
}
catch (AggregateException ae)
{foreach (var e in ae.InnerExceptions){Console.WriteLine($"Exception: {e.Message}");}
}
1.4 協調任務:async/await 模式
這是現代 .NET 異步編程的基石,它讓異步代碼看起來像同步代碼一樣直觀。
- async: 修飾方法,表明該方法包含異步操作。
- await: 用于等待一個 Task 完成。在 await 時,當前線程會被釋放回線程池,而不是被阻塞。當 Task 完成后,該方法會在線程池線程上恢復執行。
public async Task<int> GetWebsiteLengthAsync(string url)
{// 注意:不要在生產環境使用 HttpClient 這種方式,這里僅為示例。using (var httpClient = new HttpClient()){// await 會釋放當前線程(如UI線程),去處理其他工作(如響應用戶點擊)string content = await httpClient.GetStringAsync(url);// 當下載完成后,執行會在這里恢復(可能在另一個線程池線程上)return content.Length;}
}// 調用異步方法
async void Button_Click(object sender, EventArgs e)
{int length = await GetWebsiteLengthAsync("https://example.com");MessageBox.Show($"Length is: {length}");
}
優勢
- 非阻塞: 在等待 I/O 操作(如網絡請求、文件讀寫)時,不占用任何線程, scalability(可擴展性)極高。
- 清晰的代碼流: 避免了復雜的回調地獄(Callback Hell)。
2. 線程間通信
當多個線程需要訪問共享數據或協調行動時,就需要線程間通信。核心挑戰是線程安全。
2.1 共享內存與競態條件
最簡單的通信方式是共享變量,但這會導致競態條件。
private int _counter = 0;void UnsafeIncrement()
{_counter++; // 這不是原子操作,可能被線程切換打斷
}
2.2 同步原語:確保線程安全
.NET 提供了豐富的同步原語來控制對共享資源的訪問。
- lock 語句(Monitor 類): 最常用的機制,確保代碼塊在任何時候只被一個線程執行。
private readonly object _lockObject = new object();
private int _safeCounter = 0;void SafeIncrement()
{lock (_lockObject) // 一次只允許一個線程進入{_safeCounter++;}
}
注意: 鎖定對象應為 private readonly 的引用類型。
- Interlocked 類: 提供簡單的原子操作,性能比 lock 更高。
Interlocked.Increment(ref _safeCounter); // 原子性地 +1
Interlocked.Exchange(ref _value, newValue); // 原子性地交換值
- Mutex 和 Semaphore:
- Mutex: 類似于 lock,但可以跨進程使用(系統級鎖)。
- Semaphore / SemaphoreSlim: 允許指定數量的線程同時訪問一個資源池。例如,限制只有 5 個線程可以同時訪問數據庫。
- ManualResetEvent / AutoResetEvent: 用于線程間的信號通知。一個線程可以 WaitOne() 等待信號,另一個線程可以 Set() 發出信號。
- Barrier / CountdownEvent: 用于協調多個線程,讓它們在某個點同步。
2.3 線程安全的數據結構
.NET 在 System.Collections.Concurrent 命名空間中提供了一系列線程安全的集合。
- ConcurrentDictionary<TKey, TValue>
- ConcurrentQueue< T>
- ConcurrentStack< T>
- ConcurrentBag< T>
- BlockingCollection< T>
這些集合內部實現了高效的同步機制,可以在大多數情況下避免手動加鎖。
private ConcurrentDictionary<string, int> _userScores = new ConcurrentDictionary<string, int>();void UpdateScore(string userId, int points)
{// 無需手動加鎖!_userScores.AddOrUpdate(userId, points, (key, oldValue) => oldValue + points);
}
2. 多線程編程模式與最佳實踐
2.1 模式
- 生產者/消費者模式: 一個或多個線程(生產者)生成數據并放入共享隊列,一個或多個線程(消費者)從隊列中取出并處理數據。可以使用 BlockingCollection 輕松實現。
- Fork/Join 模式: 將一個大任務拆分成多個小任務(Fork),并行執行,最后等待所有結果并合并(Join)。Parallel.For/ForEach 和 Task.WhenAll 是實現此模式的利器。
2.1 最佳實踐與常見陷阱
- 避免死鎖:
- 原因: 兩個或更多線程互相等待對方釋放鎖。
- 預防: 按固定的全局順序獲取鎖;使用 Monitor.TryEnter 并設置超時;盡量減少鎖的持有時間。
- 警惕線程池的過度訂閱: 不要創建成千上萬的短時 Task,這會導致線程池創建大量線程,上下文切換開銷巨大。對于 CPU 密集型任務,任務數量不應大幅超過 CPU 核心數。
- 不要阻塞線程池線程: 在線程池線程上執行同步的 I/O 操作或長時間 CPU 計算會耗盡線程池,影響整個應用程序的響應能力。對于 I/O 操作,始終使用 async/await。
4.** 使用 Cancellation Tokens**: 提供一種標準機制來取消異步操作。
var cts = new CancellationTokenSource();
CancellationToken token = cts.Token;Task longRunningTask = Task.Run(() =>
{while (true){token.ThrowIfCancellationRequested(); // 如果取消請求了,則拋出 OperationCanceledException// ... 做一點工作}
}, token);// 在某個地方取消操作
cts.CancelAfter(5000); // 5秒后取消
- 訪問 UI 控件: 在 WPF/WinForms 中,只有 UI 線程才能更新 UI 控件。從非 UI 線程更新 UI 會引發異常。必須使用 Dispatcher.Invoke (WPF) 或 Control.Invoke (WinForms) 來封送調用回 UI 線程。
// 在 WPF 中
await Task.Run(() => DoHeavyWork());
// 現在回到 UI 線程了,可以安全更新 UI
textBox.Text = "Done!";// 如果在另一個上下文中,需要顯式調用 Dispatcher
Dispatcher.Invoke(() => { textBox.Text = "Done!"; });
總結
- 基礎: 理解 Thread 類。
- 現代方式: 優先使用 Task 和 TPL,默認使用線程池,效率更高。
- 異步 I/O: 對于 I/O 密集型操作,始終使用 async/await,以釋放線程,獲得極高的可擴展性。
- 線程安全: 使用 lock、Interlocked 或并發集合來保護共享數據。
- 協調與通信: 使用同步原語(如 Event、Barrier)和模式(生產者/消費者)來協調多線程工作。
- 避免陷阱: 警惕死鎖、過度訂閱和阻塞線程池線程。
2.2 補充
線程間通信的本質是什么?
多個線程在同一個進程內運行,共享進程的整個內存空間。因此,從廣義上講,任何一個線程寫入內存的數據,理論上都可以被其他線程讀取到。
所以,線程間通信的“通信”二字,其本質是:
- 數據傳遞:一個線程生產/計算出的數據,如何安全地交給另一個線程處理。
- 狀態同步:一個線程如何知道另一個線程已經完成了某項工作或進入了某種狀態。
- 協調行動:多個線程如何步調一致地協作,避免“混亂”(如競態條件)和“死等”(如死鎖)。
核心挑戰:由于操作系統線程調度的不確定性,你永遠不知道一個線程在執行到哪條指令時會被掛起,另一個線程會開始執行。這種交錯執行如果處理不當,就會導致數據損壞、結果錯誤等線程安全問題。
如何進行線程間通信?
.NET 提供了多種機制來實現安全高效的線程間通信,主要分為三大類:
- 共享內存(最常用,但最危險)
這是最直觀的方式:多個線程讀寫同一個變量或數據結構。
- 如何進行:簡單地創建一個所有線程都能訪問的字段、屬性或靜態變量。
- 巨大風險:直接共享內存會引發競態條件。
// 危險的共享內存示例
public class UnsafeExample
{private int _counter = 0; // 共享內存public void Increment(){_counter++; // 這不是原子操作!// 它可能被分解為:讀取 -> 加1 -> 寫入// 線程A可能在“讀取”后被打斷,線程B也完成了“讀取”,然后兩者都寫入,導致只加了一次。}
}
-
如何安全地使用:必須使用同步原語來保護對共享內存的訪問,確保某一時刻只有一個線程能操作它。
- lock 語句:最常用的工具。
private readonly object _lockObj = new object(); private int _safeCounter = 0;public void SafeIncrement() {lock (_lockObj) // 一次只允許一個線程進入此代碼塊{_safeCounter++;} }
- Interlocked 類:為簡單的數學操作提供原子性,性能更高。
Interlocked.Increment(ref _safeCounter); // 原子性地完成整個“讀取-修改-寫入”操作
- Monitor 類:lock 語句的底層實現。
- Mutex:類似于鎖,但可以跨進程使用。
- 信號機制(用于協調和通知)
當一個線程需要“等待”另一個線程完成某項工作后才能繼續時,就需要信號機制。它不直接傳遞數據,而是傳遞“事件已發生”的信號。
- 如何進行:一個線程等待一個信號,另一個線程發出信號。
- 常見類型:
- EventWaitHandle 及其子類:
- AutoResetEvent:像一個旋轉門,一次只允許一個線程通過。Set() 一次只釋放一個等待的線程,然后自動重置為無信號狀態。
- ManualResetEvent:像一個大門,Set() 打開大門,釋放所有等待的線程;直到調用 Reset() 才會關上大門。
// 使用 AutoResetEvent 進行線程協調 AutoResetEvent _waitHandle = new AutoResetEvent(false); // 初始狀態為無信號void ThreadA() {// 做一些準備工作..._waitHandle.Set(); // 發出信號:“我的工作完成了,你可以繼續了” }void ThreadB() {// 等待 ThreadA 的準備信號_waitHandle.WaitOne(); // 阻塞在此,直到收到信號// 收到信號,繼續執行... }
- Semaphore / SemaphoreSlim:類似于一個計數器,用于控制同時訪問某一資源的線程數量。例如,只允許 3 個線程同時訪問數據庫連接池。
- Barrier:用于讓多個線程在某個時間點同步,所有線程都到達這個點后,才一起繼續執行。適合分階段計算的場景。
- CountdownEvent:初始化一個計數,每次有線程完成工作時計數減一,當計數為 0 時,釋放所有等待的線程。
- EventWaitHandle 及其子類:
- 消息傳遞(更高級、更安全的模式)
這種模式解耦了線程,線程之間不直接共享內存,而是通過一個“中間人”(通常是隊列)來傳遞數據“消息”。生產者線程放入消息,消費者線程取出消息。
- 如何進行:使用生產者/消費者模式。
- .NET 提供的強大工具:System.Collections.Concurrent 命名空間下的線程安全集合。
-
BlockingCollection:一個提供了阻塞和邊界功能的線程安全集合。它是實現生產者/消費者模式的最佳工具。
// 創建一個最多容納10個項目的阻塞集合 BlockingCollection<string> _messageQueue = new BlockingCollection<string>(10);// 生產者線程 void Producer() {while (true){string message = GenerateMessage();_messageQueue.Add(message); // 如果隊列滿了,Add 會阻塞生產者}_messageQueue.CompleteAdding(); // 通知消費者不會再生產了 }// 消費者線程 void Consumer() {// GetConsumingEnumerable() 會在沒有數據時阻塞消費者,并在 CompleteAdding() 且隊列空后自動結束foreach (var message in _messageQueue.GetConsumingEnumerable()){ProcessMessage(message);} }
-
ConcurrentQueue, ConcurrentStack, ConcurrentBag, ConcurrentDictionary<TKey, TValue>:這些是線程安全的集合,可以在不加鎖的情況下被多個線程同時讀寫,但它們本身不提供阻塞功能。
-
總結與最佳實踐
通信機制 | 如何實現 | 適用場景 | 優點 | 缺點 |
---|---|---|---|---|
共享內存 | 共享變量 + 鎖/同步原語 | 高頻、簡單的數據共享 | 性能高、直觀 | 容易死鎖、難以編寫和維護 |
信號機制 | EventWaitHandle, Semaphore, Barrier | 線程間的協調、通知、同步 | 目的明確,易于理解 | 不直接傳遞數據,容易錯過信號 |
消息傳遞 | BlockingCollection< T > + 并發集合 | 生產者/消費者、解耦復雜任務 | 安全性高、解耦、易于擴展 | 有一定的性能開銷(入隊/出隊) |
現代 .NET 多線程編程的最佳實踐:
-
優先選擇消息傳遞模式:使用 BlockingCollection 或 Channel (.NET Core 3.0+) 可以極大地減少對鎖的依賴,從而避免死鎖等問題,代碼也更清晰。
-
避免共享狀態:如果可能,盡量設計無狀態的操作,讓每個線程只處理自己的數據。
-
使用高級抽象:優先使用 Task、Parallel 循環和 PLINQ,而不是手動管理 Thread 對象。它們底層使用線程池,效率更高。
-
善用異步編程:對于 I/O 密集型操作(如文件、網絡),使用 async/await 而不是創建阻塞線程,這樣可以釋放線程去處理其他請求,大大提高應用程序的吞吐量。
-
始終牢記線程安全:只要存在共享,第一反應就應該是“如何同步”。