做.net开发久了,面试问这些问的少,就没有怎么准备。有一次面试博赛信息时,面试官提到了,在此记录一下。话说平时在webapi里很少用到锁相关的,也尽量用线程并发安全的数据结构。实在要用锁,就简单的用了一下lock就不管其它的的了。 C# 中用于线程同步的原语确实不少,每种都有其特定的适用场景。下面我为你梳理一下主要的同步原语、它们的核心作用以及代码示例,希望能帮你更好地理解和运用它们。
| 同步原语 | 核心特点 | 适用场景 | 注意事项 |
|---|---|---|---|
**lock关键字** | 语法简单,自动释放锁,可重入(同一线程可多次获取) | 进程内多线程同步,保护共享资源,如计数器、状态标志等 | 锁对象需为 private readonly object类型,临界区代码应尽量精简 |
**Monitor类** | **lock的底层实现**,提供更灵活的控制(如超时) | 需要手动控制锁或设置获取超时的场景 | 需配合 try-finally确保锁释放,避免死锁 |
**Mutex(互斥体)** | 内核级对象,支持跨进程同步,重量级 | 保护跨进程共享的资源,如文件、共享内存等 | 性能开销较大,通常进程内同步不首选 |
**SemaphoreSlim** | 轻量级信号量,限制同时访问资源的线程数量 | 资源池(如数据库连接池)、限流、控制最大并发数 | 适用于进程内同步,WaitAsync支持异步等待 |
**ReaderWriterLockSlim** | 读写分离,允许多个读线程并发,写线程独占 | 读多写少的场景,如缓存、配置管理 | 注意避免写线程饥饿问题 |
**AutoResetEvent** | 事件通知,自动重置,一次只释放一个等待线程 | 线程间信号通知,如任务完成后通知另一线程 | 需注意信号可能被遗漏 |
**ManualResetEvent** | 事件通知,手动重置,释放后所有等待线程可继续,直到手动重置 | 线程间广播通知,如初始化完成后通知所有工作线程 | 需手动调用 Reset方法才能重新阻塞线程 |
**CountdownEvent** | 等待多个操作完成 | 需要等待一组任务全部完成后再继续的场景 | 计数减至零时释放所有等待线程 |
**SpinLock** | 忙等待(自旋),避免线程切换开销,适用于极短的临界区 | 临界区代码执行非常快(纳秒或微秒级)的场景 | 不适合耗时操作或I/O操作,否则浪费CPU |
**Interlocked** | 提供基本的原子操作(如递增、递减、交换、比较交换),无需显式锁 | 简单的整数、变量的原子操作,性能极高 | 仅支持简单数据类型,复杂操作仍需其他同步机制 |
下面是这些同步原语的简要说明和代码示例:
🔒 1. lock关键字
这是最常用、最简单的同步机制,它确保了同一时刻只有一个线程能进入临界区代码块。
private readonly object _lockObj = new object();
private int _counter = 0;
public void Increment()
{
lock (_lockObj) // 进入锁
{
_counter++; // 临界区
} // 自动释放锁
}
🔍 2. Monitor类
lock语句的底层就是通过 Monitor类实现的。 它提供了更灵活的控制,比如设置超时时间。
private readonly object _monitorObj = new object();
public void TryDoSomething()
{
bool lockAcquired = false;
try
{
// 尝试在100毫秒内获取锁
Monitor.TryEnter(_monitorObj, 100, ref lockAcquired);
if (lockAcquired)
{
// 临界区
}
else
{
// 获取锁超时的处理
}
}
finally
{
if (lockAcquired)
{
Monitor.Exit(_monitorObj); // 必须手动释放
}
}
}
🌐 3. Mutex(互斥体)
Mutex是内核级别的锁,可以用于跨进程的同步。
// 命名的 Mutex 可以被系统内其他进程识别
private static Mutex _mutex = new Mutex(false, "Global\MyAppMutex");
public void CrossProcessSafeMethod()
{
_mutex.WaitOne(); // 请求拥有互斥体
try
{
// 操作跨进程共享的资源,如一个文件
}
finally
{
_mutex.ReleaseMutex(); // 释放
}
}
🚦 4. SemaphoreSlim/ Semaphore
信号量用于控制同时访问某个资源的线程数量。 SemaphoreSlim是轻量版,推荐在进程内使用。
// 允许最多3个线程同时访问资源池
private SemaphoreSlim _semaphore = new SemaphoreSlim(3, 3);
public async Task AccessResourceAsync()
{
await _semaphore.WaitAsync(); // 等待进入
try
{
// 使用资源,最多3个线程同时在这里
}
finally
{
_semaphore.Release(); // 释放,允许其他线程进入
}
}
📖 5. ReaderWriterLockSlim
此锁允许多个线程同时读取,但写入时独占,非常适合读多写少的场景。
private ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();
private Dictionary<int, string> _cache = new Dictionary<int, string>();
public string Read(int key)
{
_rwLock.EnterReadLock(); // 获取读锁
try
{
return _cache[key];
}
finally
{
_rwLock.ExitReadLock();
}
}
public void Write(int key, string value)
{
_rwLock.EnterWriteLock(); // 获取写锁
try
{
_cache[key] = value;
}
finally
{
_rwLock.ExitWriteLock();
}
}
🚏 6. AutoResetEvent和 ManualResetEvent
用于线程间的信号通知。 AutoResetEvent在信号被设置后自动重置,只释放一个等待线程;ManualResetEvent则需要手动重置,释放后所有等待线程都可继续。
// AutoResetEvent 示例
private AutoResetEvent _autoEvent = new AutoResetEvent(false);
void Thread1()
{
// 做一些工作...
_autoEvent.Set(); // 发送信号,释放一个等待线程
}
void Thread2()
{
_autoEvent.WaitOne(); // 等待信号
// 收到信号后继续执行...
}
// ManualResetEvent 示例
private ManualResetEvent _manualEvent = new ManualResetEvent(false);
void Initialize()
{
// 执行初始化...
_manualEvent.Set(); // 初始化完成,通知所有等待线程
}
void WorkerThread()
{
_manualEvent.WaitOne(); // 等待初始化完成
// 初始化完成后所有线程同时继续...
}
🔢 7. Interlocked类
提供了一系列静态方法用于执行原子操作,这些操作是天生线程安全的,无需使用锁,性能极高。
private int _atomicCounter = 0;
public void IncrementAtomically()
{
// 原子递增,返回递增后的值
Interlocked.Increment(ref _atomicCounter);
}
public int CompareExchangeExample(int newValue, int compareTo)
{
// 如果当前值等于compareTo,则替换为newValue。总是返回原始值。
return Interlocked.CompareExchange(ref _atomicCounter, newValue, compareTo);
}
⚡ 8. SpinLock(自旋锁)
当线程无法获取锁时,它会进行“忙等待”(自旋),而不是立刻被阻塞。这避免了线程上下文切换的开销,但会持续占用CPU。 仅适用于临界区代码执行速度极快(例如,只是几条简单指令)的场景。
private SpinLock _spinLock = new SpinLock();
public void VeryFastOperation()
{
bool lockTaken = false;
try
{
_spinLock.Enter(ref lockTaken); // 获取自旋锁
// 执行非常快速的操作,例如修改一个标志位
}
finally
{
if (lockTaken)
{
_spinLock.Exit(); // 释放自旋锁
}
}
}
🎯 如何选择同步原语?
选择哪种同步机制,主要考虑以下几点:
- 同步范围:是进程内 (
lock,Monitor,SemaphoreSlim,ReaderWriterLockSlim) 还是需要跨进程 (Mutex,Semaphore)? - 操作粒度:是简单的变量操作 (
Interlocked)?还是短暂的代码块 (lock,SpinLock)?或是需要控制并发数量 (SemaphoreSlim)? - 读写模式:是纯写入 (
lock) 还是读多写少 (ReaderWriterLockSlim)? - 性能需求:临界区执行时间极短,想避免上下文切换?(考虑
SpinLock,但要谨慎)。 - 协作方式:是否需要线程间发送信号、等待事件?(使用
AutoResetEvent,ManualResetEvent)。
通用建议:
- **首选
lock**:对于大多数进程内的同步需求,lock语句因其简单性和可靠性通常是首选。 - 避免过度同步:只在必要的时候保护共享资源,锁的粒度要尽可能小,以减少对并发性能的影响。
- 警惕死锁:确保多个锁的获取顺序一致,并考虑使用
Monitor.TryEnter来提供超时机制。