在现代应用中,充分利用多核心 CPU 的能力是提升性能的关键途径。基础教程中我们已经了解了 Task 和 async/await 的基本用法,但多线程编程远比这更加复杂。本文将深入探讨 System.Threading 层面的核心概念:线程生命周期、线程池、同步原语(锁、信号量、屏障)、以及如何避免死锁和竞态条件。
线程 vs 任务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| Thread thread = new Thread(() => { Console.WriteLine($"线程 {Thread.CurrentThread.ManagedThreadId} 运行中"); Thread.Sleep(1000); }); thread.IsBackground = true; thread.Start(); thread.Join();
ThreadPool.QueueUserWorkItem(state => { Console.WriteLine($"线程池线程 {Thread.CurrentThread.ManagedThreadId}"); });
await Task.Run(() => Console.WriteLine("Task 运行中"));
|
核心区别:Thread 是 OS 层面的概念,每个线程消耗大量资源;Task 是 .NET 层面的异步工作单元,默认由线程池调度,创建和销毁成本低得多。
线程间同步
1. lock 语句
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| private readonly object _lock = new(); private int _counter;
public void Increment() { lock (_lock) { _counter++; } }
public void IncrementExpanded() { bool lockTaken = false; try { Monitor.Enter(_lock, ref lockTaken); _counter++; } finally { if (lockTaken) Monitor.Exit(_lock); } }
|
2. 自旋锁(SpinLock)—— 低锁竞争场景
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| private SpinLock _spinLock = new(false);
public void SafeOperation() { bool lockTaken = false; try { _spinLock.Enter(ref lockTaken); } finally { if (lockTaken) _spinLock.Exit(); } }
|
SpinLock 让线程忙等待而不是进入休眠,适合锁持有时间极短(纳秒级)且锁竞争低的场景。错误使用会导致 CPU 100%。
3. 读写锁(ReaderWriterLockSlim)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| private readonly ReaderWriterLockSlim _rwLock = new(); private readonly Dictionary<string, string> _cache = new();
public string? Read(string key) { _rwLock.EnterReadLock(); try { return _cache.TryGetValue(key, out var value) ? value : null; } finally { _rwLock.ExitReadLock(); } }
public void Write(string key, string value) { _rwLock.EnterWriteLock(); try { _cache[key] = value; } finally { _rwLock.ExitWriteLock(); } }
|
多个线程可以同时持有读锁,但写锁是独占的。适用于读多写少的场景。
4. Mutex —— 跨进程同步
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| using var mutex = new Mutex(true, "Global\\MyAppUniqueName"); if (!mutex.WaitOne(TimeSpan.Zero)) { Console.WriteLine("程序已在运行"); return; }
try { } finally { mutex.ReleaseMutex(); }
|
5. SemaphoreSlim —— 限制并发数量
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| private readonly SemaphoreSlim _semaphore = new(3, 3);
async Task ProcessRequestAsync(HttpRequest request) { await _semaphore.WaitAsync(); try { } finally { _semaphore.Release(); } }
|
6. Barrier —— 协同并行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| Barrier barrier = new(3, b => { Console.WriteLine($"阶段 {b.CurrentPhaseNumber} 完成"); });
for (int i = 0; i < 3; i++) { int id = i; Task.Run(() => { Console.WriteLine($"参与者 {id} 到达阶段 1"); barrier.SignalAndWait(); Console.WriteLine($"参与者 {id} 到达阶段 2"); barrier.SignalAndWait(); }); }
|
7. CountdownEvent —— 等待所有完成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| using var countdown = new CountdownEvent(3);
for (int i = 0; i < 3; i++) { int id = i; Task.Run(() => { Thread.Sleep(id * 200); Console.WriteLine($"{id} 完成"); countdown.Signal(); }); }
countdown.Wait(); Console.WriteLine("全部完成");
|
避免死锁
经典的死锁例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| object lockA = new(); object lockB = new();
Task t1 = Task.Run(() => { lock (lockA) { Thread.Sleep(100); lock (lockB) { } } });
Task t2 = Task.Run(() => { lock (lockB) { Thread.Sleep(100); lock (lockA) { } } });
await Task.WhenAll(t1, t2);
|
预防策略
- 固定锁顺序:始终以相同顺序获取锁
- 使用超时机制:
1 2 3 4 5 6 7 8 9 10 11 12
| if (Monitor.TryEnter(lockA, TimeSpan.FromSeconds(1))) { try { if (Monitor.TryEnter(lockB, TimeSpan.FromSeconds(1))) { try { } finally { Monitor.Exit(lockB); } } } finally { Monitor.Exit(lockA); } }
|
- 减少锁粒度:分散到大范围的细粒度锁
- 使用无锁数据结构:
ConcurrentDictionary、Interlocked 等
- 避免锁中嵌套锁
volatile 与内存模型
1 2 3 4 5 6 7 8
| private volatile bool _shouldStop;
void Worker() { while (!_shouldStop) { } }
void Stop() => _shouldStop = true;
|
volatile 确保:
- 读取时总是从内存中取最新值(不缓存到 CPU 寄存器)
- 防止编译器/CPU 重排序越过 volatile 操作
在现代 .NET 中 volatile 已较少直接使用,Interlocked 和 lock 提供了更强的保证。Volatile.Read/Write 提供了更细粒度的控制。
Interlocked 原子操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| private long _counter;
public void AtomicIncrement() { Interlocked.Increment(ref _counter); }
public void AtomicAdd(int value) { Interlocked.Add(ref _counter, value); }
public long ExchangeIfGreater(long newValue) { long initial; long computed; do { initial = _counter; computed = Math.Max(initial, newValue); } while (Interlocked.CompareExchange(ref _counter, computed, initial) != initial); return computed; }
|
Interlocked 操作直接映射到 CPU 的原子指令(如 lock cmpxchg),无锁且零开销上下文切换。
ThreadLocal —— 线程本地存储
1 2 3 4 5 6 7 8 9 10 11
| private static readonly ThreadLocal<int> _threadId = new(() => Thread.CurrentThread.ManagedThreadId);
private static readonly ThreadLocal<Random> _random = new(() => new Random(Thread.CurrentThread.ManagedThreadId));
Parallel.For(0, 100, i => { Console.WriteLine($"线程 {_threadId.Value} 生成随机数: {_random.Value.Next()}"); });
|
总结:多线程编程需要深刻理解 OS 线程模型和 .NET 提供的各种同步原语。选择合适的同步工具(lock/SpinLock/Semaphore/Barrier)、遵循固定的锁顺序避免死锁、使用原子操作或无锁数据结构提升性能,是编写健壮并发代码的关键。在多数场景下,优先使用更高级的 TPL/Dataflow 抽象而不要直接操作 Thread,但在理解底层原理之前,高级抽象也容易误用。