首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C# 多線程與同步機制深度解析:從 Thread 到 Task 的演進

     編輯:關於C語言

前言

多線程程式設計常被視為高難度領域,但掌握核心概念後其實相當直觀。本文從作業系統線程基礎出發,層層深入講解 C# 線程建立、同步原語、非同步演進,並提供實戰範例與最佳實踐指南。

基本概念:什麼是線程?

  • 定義:線程是處理器調度的基本單位,代表程式執行的一條路徑
  • 單線程:程式碼循序執行,同一時間只做一件事
  • 多線程:多條執行路徑「同時」進行(單核透過時間片輪轉,多核真正並行)
  • 進程 vs 線程:進程擁有獨立記憶體空間;線程隸屬進程,共享進程資源(堆疊、全域變數、檔案控制代碼)

C# 線程基礎 API

建立與啟動

using System.Threading;

// 傳統 Thread 類別
Thread t1 = new Thread(() => 
{
    for (int i = 0; i < 10; i++)
    {
        Thread.Sleep(1000);
        Console.WriteLine($"Thread: {i}");
    }
});
t1.Name = "WorkerThread";
t1.IsBackground = true; // 重要:主程式結束時自動終止
t1.Start();

// 參數化啟動
Thread t2 = new Thread(DoWork);
t2.Start("Hello"); // 傳遞物件參數

void DoWork(object data)
{
    string msg = (string)data;
    Console.WriteLine(msg);
}

Thread 關鍵屬性與方法

成員說明
CurrentThread靜態屬性,取得目前執行的線程
ManagedThreadId唯一識別碼(除錯用)
IsAlive線程是否正在執行
IsBackground背景線程:不阻止程式結束
Priority排程優先權 (Lowest~Highest)
ThreadState狀態旗標
Sleep(ms)靜態方法,讓當前線程休眠
Join()等待線程結束
Abort()已過時,強制終止,易造成資源洩漏

線程同步:解決共享資源競爭

為什麼需要同步?

// 經典競爭條件範例
int counter = 0;
for (int i = 0; i < 10; i++)
{
    new Thread(() => 
    {
        for (int j = 0; j < 10000; j++) counter++;
    }).Start();
}
Thread.Sleep(1000);
Console.WriteLine(counter); // 預期 100000,實際通常 < 100000

同步原語比較

原語命名空間適用場景特點
lock / MonitorSystem.Threading簡單互斥、臨界區短語法糖、可重入、死鎖風險低
MutexSystem.Threading跨進程同步核心物件、較重、可命名
Semaphore / SemaphoreSlimSystem.Threading限制並發數量計數器機制、Slim 版輕量
AutoResetEvent / ManualResetEventSystem.Threading線程間訊號通知EventWaitHandle 衍生、等待/通知模式
ReaderWriterLockSlimSystem.Threading讀多寫少支援多讀單寫、效能優於 lock
InterlockedSystem.Threading原子操作硬體級原子、無鎖、效能最佳
Concurrent 集合System.Collections.Concurrent執行緒安全集合無鎖/細粒度鎖、開箱即用

實戰範例:ManualResetEvent 等待訊號


ManualResetEvent mre = new ManualResetEvent(false); // 初始非發信號狀態

// 背景工作線程
Thread worker = new Thread(() =>
{
    Console.WriteLine("Worker: 等待啟動訊號...");
    mre.WaitOne(); // 阻塞直到收到訊號
    Console.WriteLine("Worker: 收到訊號,開始工作");
    
    for (int i = 0; i < 5; i++)
    {
        Thread.Sleep(500);
        Console.WriteLine($"Worker: 處理中 {i}");
    }
    Console.WriteLine("Worker: 完成");
});
worker.Start();

// 主線程做其他事...
Thread.Sleep(2000);
Console.WriteLine("Main: 發送啟動訊號");
mre.Set(); // 發送訊號,喚醒所有等待者

// 等待背景線程結束
worker.Join();
Console.WriteLine("Main: 程式結束");

生產者消費者模式 (BlockingCollection)


var queue = new BlockingCollection(10); // 容量 10

// 生產者
Task.Run(() =>
{
    for (int i = 0; i < 20; i++)
    {
        queue.Add(i); // 滿時自動阻塞
        Console.WriteLine($"Produced: {i}");
        Thread.Sleep(100);
    }
    queue.CompleteAdding(); // 標記完成
});

// 消費者
Task.Run(() =>
{
    foreach (var item in queue.GetConsumingEnumerable()) // 空時阻塞,CompleteAdding 後結束
    {
        Console.WriteLine($"Consumed: {item}");
        Thread.Sleep(200);
    }
    Console.WriteLine("Consumer 結束");
});

非同步程式設計演進:Task 與 async/await

為什麼從 Thread 進化到 Task?

  • Thread:直接對應 OS 線程,建立/銷毀成本高,難以組合、例外處理複雜
  • ThreadPool:線程池重用線程,但 API 以回調為主,易陷入「回調地獄」
  • Task:代表「未來會完成的工作」,支援組合、延續、例外傳播、取消、進度報告
  • async/await:編譯器生成狀態機,讓非同步程式碼像同步一樣易讀

Task 基礎用法


// 建立與啟動
Task t1 = Task.Run(() => DoWork()); // ThreadPool 執行
Task t2 = Task.Factory.StartNew(() => DoWork(), TaskCreationOptions.LongRunning); // 專用線程

// 等待與結果
int result = await Task.Run(() => Compute()); // 非同步等待,不阻塞線程
int result2 = Task.Run(() => Compute()).Result; // 同步阻塞等待(避免在 UI 執行緒用)

// 組合
Task all = Task.WhenAll(task1, task2, task3); // 全部完成
Task any = Task.WhenAny(task1, task2);        // 任一完成

// 取消
var cts = new CancellationTokenSource();
Task.Run(() => LongWork(cts.Token), cts.Token);
cts.CancelAfter(5000); // 5 秒後取消

async/await 核心規則

  1. async 方法回傳 Task/Task<T>/void (僅事件處理器)
  2. await 只能在 async 方法中使用
  3. await 會「暫停」方法,釋放線程,完成後續在原上下文(或 ThreadPool)執行
  4. 避免 .Result、.Wait() 造成死結

現代最佳實踐指南

✅ 應該做

  • IO 密集型操作(檔案、網路、資料庫)使用 async/await
  • CPU 密集型運算使用 Task.Run 卸載到 ThreadPool
  • 使用 SemaphoreSlim 限制並發度(如同時下載數)
  • 使用 CancellationToken 支援取消
  • 使用 ConcurrentDictionary 等無鎖集合
  • 正確處理例外:try/catch 包裹 await

❌ 應避免

  • 手動建立 new Thread()(除非長時間運行、需專用線程)
  • 使用 Thread.Abort()、Thread.Suspend/Resume
  • 在 lock 中執行耗時 IO 或呼叫外部程式碼
  • 同步等待非同步方法(.Result/.Wait())
  • 忽略 ConfigureAwait(false)(程式庫開發時)

效能比較:同步 vs 非同步 Web 伺服器


// 同步版:每個請求佔用一條線程
public string SyncDownload(string url)
{
    using (var client = new WebClient())
    return client.DownloadString(url); // 阻塞線程
}

// 非同步版:釋放線程處理其他請求
public async Task AsyncDownload(string url)
{
    using (var client = new HttpClient())
    return await client.GetStringAsync(url); // 釋放線程
}

// 壓測結果(1000 並發請求,每請求 100ms 延遲):
// 同步:需 1000 條線程,記憶體 ~200MB,排程開銷大
// 非同步:~20 條線程,記憶體 ~20MB,吞吐量高 10x+

總結

C# 多線程發展脈絡:Thread → ThreadPool → Task → async/await。核心心法不變:將阻塞操作移出關鍵路徑,善用硬體並行能力。現代開發以 Task-based Asynchronous Pattern (TAP) 為主,搭配 SemaphoreSlim、BlockingCollection、Concurrent* 等高階同步工具,能寫出高效能、可維護、可擴展的並行程式。記住:正確性優於效能,簡單優於巧妙。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved