并发工具
lock、Lazy<T>、Interlocked、SemaphoreSlim/ReaderWriterLock、volatile.
1.lock -最基本的同步方式
lock 是 .NET中最常用的同步工具,用来确保一个线程在执行特定代码时,其它线程无法同时执行这段代码.
private readonly object _lockObj = new object();
public void CriticalSection()
{
lock(_lockObj)
{
// 只有一个线程能够进入这里
// 其他线程在这里会被阻塞
// 执行临界操作
}
}工作原理:
- lock 是对 Monitor.Enter 和 Monitor.Exit 的封装.
- 每次进入 lock 时,会 获取对象的锁, 并且在操作结束后,释放锁.
注意事项:
- 锁定的是对象实例(通常是 readonly 对象, 避免锁对象被修改)。
- 如果 锁的粒度过大 (例如锁住整个方法), 可能导致性能瓶颈。
- 避免死锁: 确保在所有地方都用相同的顺序获取锁。
2.Lazy - 延迟初始化
Lazy
Lazy<MyClass> lazyObject = new Lazy<MyClass>(() => new MyClass());
public MyClass GetObject()
{
return lazyObject.Value; // 只有第一次访问时才会创建 MyClass 实例
}工作原理:
- 当访问 lazyObject.Value 时, Lazy
会判断是否已初始化对象。 - 如果未初始化, 会 安全地初始化 (单线程中也能保证线程安全)。
线程安全:
Lazy
3.Interlocked - 原子操作
Interlocked提供了一些 原子操作,用于多线程中避免竞争条件。它确保对某些基本数据类型(如 int )的 操作是 不可分割的。
常见用法:
- Interlocked.Add(ref int location, int value)
- Interlocked.CompareExchange(ref T location, T value, T comparand)
- Interlocked.Exchange(ref T location, T value)
- Interlocked.Decrement(ref int location)
- interlocked.Increment(ref int location)
Interlocked 可以用于简单的数值类型 ( 如 int) , 使用于技术,累加, 比较和交换等操作。它能在多线程中避免不必要的锁。
private int counter = 0;
public void IncrementCounter()
{
Interlocked.Increment(ref counter); // 线程安全地增加计数器
}适用场景:
- 用于数值类型操作, 避免显式锁。
- 适用于多线程下的技术、标志位等操作。
4.SemaphoreSlim 和 ReaderWriterLockSlim
这两者用于更细粒度地线程同步,适用于需要 控制对资源的并发访问 的场景。
SemaphoreSlim - 信号量
SemaphoreSlim 用于 限制线程数目, 控制资源的并发访问。它是比 lock 更轻量的方式,适用于对资源数量有限的场景。
private SemaphoreSlim _semaphore = new SemaphoreSlim(3); // 允许最多 3 个线程同时访问
public async Task DoWork()
{
await _semaphore.WaitAsync();
try
{
// 只有 3 个线程可以同时访问这里
// 执行任务
}
finally
{
_semaphore.Release(); // 释放信号量
}
}适用场景:
- 限制并发线程数(例如限制数据库连接池大小)。
- 适用于高并发但有限资源的场景。
ReaderWriterLockSlim - 读写锁
ReaderWriterLockSlim 是一种 锁,适用于 读多写少 的场景。它允许多个线程同时读取,但 只有一个线程 能写入。
private ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
public void ReadData()
{
_lock.EnterReadLock(); // 获取读锁
try
{
// 多线程可以同时读取数据
}
finally
{
_lock.ExitReadLock(); // 释放读锁
}
}
public void WriteData()
{
_lock.EnterWirteLock(); // 获取写锁
try
{
// 只有一个线程可以写数据
}
finally
{
_lock.ExitWriteLock(); // 释放写锁
}
}适用场景:
- 读多写少 的数据结构 (如缓存, 数据库等)
- 多线程并发读取,写操作比较少的场景。
5.volatile - 保证内存可见性
volatitle 修饰符用于确保 变量的更新立即对所有线程可见。它主要保证的是 内存可见性,而非原子性。
private volatitle isFlagChanged;
pubic void UpdateFlag()
{
isFlagChanged = true;
}
public bool CheckFlag()
{
return isFlagChanged;
}工作原理:
- 对 volatitle 修饰的字段的写入,所有线程都能立刻看到最新的值。
- 避免 CPU 或缓存优化导致某些线程看到的值过时。
volatitle 并不保证原子性。例如,不能用 volatitle 来保证整数加法操作是线程安全的,需要配合 Interlocked或其它同步机制。
总结:
工具 线程安全性 适用场景
lock 提供互斥锁(互斥访问某些资源); 简单的临界区保护(对象、集合、文件等)
Lazy<T> 延迟初始化,线程安全; 惰性加载对象(仅初始化一次,线程安全)
Interlocked 原子操作(适合数值类型操作); 计数器、标志位、累加、比较交换等操作
SemaphoreSlim 限制并发线程数目(轻量级信号量); 控制并发访问的线程数(如连接池)ReaderWriterLockSlim 允许多个线程读、一个线程写; 读多写少的场景(如缓存)
volatile 保证内存可见性; 变量更新必须立即被所有线程看到
lock是最基本的同步工具,但它的性能开销较大,适合简单的临界区控制。Lazy<T>适用于延迟初始化,且自带线程安全保障。Interlocked提供了非常高效的原子操作,用于简单的数值类型操作,避免锁开销。SemaphoreSlim和ReaderWriterLockSlim是用于更精细的并发控制,适用于需要限制并发量和读写分离的场景。volatile用于保证字段在多线程环境中的内存可见性,但不保证原子性,不能单独依赖它来做线程安全的操作。