一、C# 线程同步的核心概念与作用
线程同步 是多线程编程中控制共享资源访问顺序 的技术,目的是解决竞态条件 (多个线程无序操作共享资源导致数据不一致),确保程序在多线程环境下的数据正确性 和行为可预测性。
核心作用:
- 保证原子性 :将多步操作(如
a = a + 1)变为 "不可分割" 的原子操作,避免中间状态被其他线程干扰。 - 保证可见性:确保一个线程对共享变量的修改,能被其他线程立即感知(避免 CPU 缓存导致的 "脏读")。
- 保证有序性:禁止编译器 / CPU 对指令的乱序优化,确保代码执行顺序符合预期。
二、C# 中常用的线程同步方式
1. 线程同步的意义(基础案例:未同步的问题)
问题场景:多个线程同时修改共享计数器,导致结果错误。
cs
using System;
using System.Threading;
class ThreadSyncDemo
{
// 共享资源:未同步的计数器
private static int _counter = 0;
static void Main()
{
Console.WriteLine("=== 未同步的计数器 ===");
// 启动10个线程同时递增计数器
for (int i = 0; i < 10; i++)
{
new Thread(IncrementCounter).Start();
}
// 等待所有线程完成(简单模拟,实际应使用Join)
Thread.Sleep(1000);
Console.WriteLine($"最终计数器值:{_counter}"); // 预期10,实际可能小于10
}
static void IncrementCounter()
{
// 非原子操作:读取→递增→写入
int temp = _counter;
Thread.Sleep(1); // 模拟耗时操作,放大竞态条件
_counter = temp + 1;
Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}:当前值={_counter}");
}
}
输出(示例):
cs
=== 未同步的计数器 ===
线程4:当前值=1
线程5:当前值=1
线程6:当前值=2
线程7:当前值=3
...
最终计数器值:8
原因 :多个线程同时读取_counter的旧值,导致递增操作被覆盖。
2. 监视器(Monitor)与 lock 关键字
lock是Monitor的语法糖,保证临界区代码互斥执行。
cs
using System;
using System.Threading;
class MonitorLockDemo
{
private static int _counter = 0;
// 锁对象:必须是引用类型,且私有只读(避免外部干扰)
private static readonly object _lockObj = new object();
static void Main()
{
Console.WriteLine("=== 使用lock的计数器 ===");
for (int i = 0; i < 10; i++)
{
new Thread(IncrementCounterWithLock).Start();
}
Thread.Sleep(1000);
Console.WriteLine($"最终计数器值:{_counter}"); // 稳定输出10
}
static void IncrementCounterWithLock()
{
// lock自动包含try-finally,确保锁释放
lock (_lockObj)
{
int temp = _counter;
Thread.Sleep(1);
_counter = temp + 1;
Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}:当前值={_counter}");
}
}
}
输出:
cs
=== 使用lock的计数器 ===
线程4:当前值=1
线程5:当前值=2
线程6:当前值=3
...
最终计数器值:10
原理 :lock会调用Monitor.Enter(_lockObj)获取锁,finally中调用Monitor.Exit(_lockObj)释放锁,确保同一时间只有一个线程进入临界区。
3. Lock(C# 13 新特性)
C# 13 引入的System.Threading.Lock,通过作用域自动管理锁,比Monitor更简洁高效。
cs
using System;
using System.Threading;
using System.Threading.Tasks;
class NewLockDemo
{
private static int _counter = 0;
// C# 13的Lock类型
private static readonly Lock _newLock = new Lock();
static async Task Main()
{
Console.WriteLine("=== 使用C# 13 Lock的计数器 ===");
// 启动10个并发任务
Task[] tasks = new Task[10];
for (int i = 0; i < 10; i++)
{
tasks[i] = Task.Run(IncrementCounterWithNewLock);
}
await Task.WhenAll(tasks);
Console.WriteLine($"最终计数器值:{_counter}"); // 稳定输出10
}
static void IncrementCounterWithNewLock()
{
// using作用域结束时自动释放锁
using (_newLock.EnterScope())
{
int temp = _counter;
Thread.Sleep(1);
_counter = temp + 1;
Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}:当前值={_counter}");
}
}
}
优势 :无需手动处理try-finally,锁生命周期与代码块一致,降低死锁风险。
4. volatile 关键字
volatile确保变量的可见性 (禁止 CPU 缓存)和有序性(禁止指令重排),但不保证原子性。
cs
using System;
using System.Threading;
class VolatileDemo
{
// volatile标记:确保线程间可见
private static volatile bool _isRunning = true;
static void Main()
{
Console.WriteLine("=== volatile示例 ===");
new Thread(Worker).Start();
// 主线程修改_isRunning
Thread.Sleep(1000);
_isRunning = false;
Console.WriteLine("主线程:已设置_isRunning=false");
}
static void Worker()
{
int count = 0;
// 若_isRunning不标记volatile,Worker可能永远无法感知到修改
while (_isRunning)
{
count++;
}
Console.WriteLine($"Worker线程:退出循环,执行次数={count}");
}
}
输出:
cs
=== volatile示例 ===
主线程:已设置_isRunning=false
Worker线程:退出循环,执行次数=...
注意 :volatile仅适用于bool、int等简单类型,复杂操作(如_counter++)仍需配合锁。
5. System.Threading.Interlocked
提供原子操作(如递增、交换),底层由 CPU 指令支持,比锁更高效。
cs
using System;
using System.Threading;
class InterlockedDemo
{
private static int _counter = 0;
static void Main()
{
Console.WriteLine("=== Interlocked原子操作 ===");
for (int i = 0; i < 10; i++)
{
new Thread(IncrementWithInterlocked).Start();
}
Thread.Sleep(1000);
Console.WriteLine($"最终计数器值:{_counter}"); // 稳定输出10
}
static void IncrementWithInterlocked()
{
// 原子递增:读取→递增→写入是一个不可分割的操作
Interlocked.Increment(ref _counter);
Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}:当前值={_counter}");
Thread.Sleep(1);
}
}
常用方法:
Increment(ref int):原子递增Decrement(ref int):原子递减Exchange(ref T, T):原子交换值CompareExchange(ref T, T, T):原子比较并交换
6. System.Threading.Mutex
Mutex(互斥锁)支持跨进程同步 ,比lock更重(涉及系统调用)。
cs
using System;
using System.Threading;
class MutexDemo
{
private static int _counter = 0;
// 命名Mutex:支持跨进程同步
private static readonly Mutex _mutex = new Mutex(false, "MyAppMutex");
static void Main()
{
Console.WriteLine("=== Mutex示例 ===");
for (int i = 0; i < 10; i++)
{
new Thread(IncrementWithMutex).Start();
}
Thread.Sleep(1000);
Console.WriteLine($"最终计数器值:{_counter}"); // 稳定输出10
}
static void IncrementWithMutex()
{
// 请求获取Mutex(阻塞直到获取)
_mutex.WaitOne();
try
{
int temp = _counter;
Thread.Sleep(1);
_counter = temp + 1;
Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}:当前值={_counter}");
}
finally
{
// 必须释放Mutex,否则其他线程永久阻塞
_mutex.ReleaseMutex();
}
}
}
场景:确保应用程序单实例运行、跨进程共享文件 / 硬件资源。
7. 重新发送事件(线程同步中的 "重试机制")
当线程获取锁失败时,通过重试 + 延迟避免频繁竞争,提升性能。
cs
using System;
using System.Threading;
class RetryEventDemo
{
private static int _counter = 0;
private static readonly object _lockObj = new object();
static void Main()
{
Console.WriteLine("=== 重新发送事件(重试机制) ===");
for (int i = 0; i < 10; i++)
{
new Thread(IncrementWithRetry).Start();
}
Thread.Sleep(2000);
Console.WriteLine($"最终计数器值:{_counter}");
}
static void IncrementWithRetry()
{
bool lockAcquired = false;
// 重试最多5次,每次失败后延迟10ms
for (int retry = 0; retry < 5 && !lockAcquired; retry++)
{
// 尝试获取锁(超时10ms)
lockAcquired = Monitor.TryEnter(_lockObj, 10);
if (lockAcquired)
{
try
{
int temp = _counter;
Thread.Sleep(1);
_counter = temp + 1;
Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}:当前值={_counter}(第{retry+1}次尝试成功)");
}
finally
{
Monitor.Exit(_lockObj);
}
}
else
{
Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}:第{retry+1}次尝试获取锁失败,重试...");
}
}
}
}
输出(示例):
cs
=== 重新发送事件(重试机制) ===
线程4:第1次尝试获取锁失败,重试...
线程5:第1次尝试获取锁成功,当前值=1
线程4:第2次尝试获取锁成功,当前值=2
...
8. 线程本地存储(TLS)
为每个线程创建独立的变量副本 ,避免共享资源竞争(无需同步)。C# 中常用ThreadLocal<T>实现。
cs
using System;
using System.Threading;
class ThreadLocalStorageDemo
{
// 每个线程有独立的计数器副本
private static ThreadLocal<int> _threadLocalCounter = new ThreadLocal<int>(() => 0);
static void Main()
{
Console.WriteLine("=== 线程本地存储 ===");
// 启动3个线程,每个线程递增自己的副本
for (int i = 0; i < 3; i++)
{
new Thread(IncrementThreadLocalCounter).Start();
}
Thread.Sleep(1000);
Console.WriteLine("主线程:所有线程执行完毕");
}
static void IncrementThreadLocalCounter()
{
for (int i = 0; i < 3; i++)
{
_threadLocalCounter.Value++;
Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}:本地计数器={_threadLocalCounter.Value}");
Thread.Sleep(100);
}
// 释放资源(可选)
_threadLocalCounter.Dispose();
}
}
输出:
cs
=== 线程本地存储 ===
线程4:本地计数器=1
线程5:本地计数器=1
线程6:本地计数器=1
线程4:本地计数器=2
线程5:本地计数器=2
...
优势:完全避免线程间竞争,适用于 "每个线程独立状态" 的场景(如数据库连接缓存)。
三、总结
C# 线程同步的核心是控制共享资源的访问顺序,不同技术的适用场景:
|-----------------------|---------------------|---------------------------------------|
| 技术 | | 核心作用 | |------| | | 适用场景 | |------| |
| lock/Monitor | 互斥访问临界区 | 大多数共享资源同步 |
| System.Threading.Lock | 简化锁管理 | |----------------| | C# 13 + 的高性能同步 | |
| volatile | 保证可见性 / 有序性 | 简单变量的线程间状态同步 |
| Interlocked | 原子操作 | 简单数值的递增 / 交换 |
| Mutex | 跨进程同步 | 多进程共享资源 |
| 重新发送事件 | |-------| | 减少锁竞争 | | 高并发场景的锁重试 |
| ThreadLocal<T> | 线程独立副本 | 每个线程的私有状态 |
补充说明(选型核心原则)
- 优先选轻量级方案:能不用锁就不用(如 ThreadLocal)→ 能用原子操作就不用锁(Interlocked)→ 能用用户态锁(lock/Lock)就不用内核态锁(Mutex);
- 避免过度同步:仅对 "真正共享的资源" 加同步,线程私有数据直接用 TLS;
- 死锁规避 :
- lock/Mutex 需保证 "锁的获取顺序一致"(如先锁 A 再锁 B,所有线程都遵循);
- 尽量缩短临界区代码(只包裹必要操作,避免在锁内做 IO / 耗时计算);
- 性能权衡 :
- 高并发计数器:优先 Interlocked,而非 lock;
- 跨进程同步:只能用 Mutex;
- 简单状态标记:用 volatile 替代 lock。