管理

C# 多线程同步机制(多种同步机制对比)

Posted on 2025-02-25 11:00  lzhdim  阅读(102)  评论(0)    收藏  举报
在C#中,lockInterlockedMonitorSpinLockWaitHandleMutexSemaphoreEventsBarrierReaderWriterLockSlim 这些同步机制虽然都用于多线程同步,但它们的底层实现、使用场景和性能特点各不相同。

1. lock 关键字

底层操作:
  • lock 是基于 Monitor 实现的语法糖。

  • 底层调用 Monitor.Enter 和 Monitor.Exit 方法。

  • 使用 lock 时,编译器会自动生成 try-finally 块,确保锁的释放。

特点:

  • 基于内核对象的同步机制(Monitor 内部使用 SyncBlock)。

  • 适用于简单的临界区保护。

  • 可能会引入死锁问题。

适用场景:

  • 简单的线程同步,保护共享资源。用于简单的线程同步,确保同一时间只有一个线程访问共享资源。

private static readonly object _lock = new object();private static int _counter = 0;
public static void IncrementCounter(){    lock (_lock)    {        _counter++;    }}

 

2. Interlocked 类

底层操作:

  • 使用 CPU 的原子指令(如 LOCK CMPXCHG)实现原子操作。

  • 直接操作内存,无需锁。

特点:

  • 轻量级,性能高。

  • 仅支持简单的原子操作(如 IncrementDecrementExchangeCompareExchange)。

  • 不适用于复杂的同步逻辑。

适用场景:

  • 对简单类型(如 intlong)进行原子操作。用于对简单类型的原子操作,如递增、递减、交换等。

private static int _counter = 0;
public static void IncrementCounter(){    Interlocked.Increment(ref _counter);}

 

3. Monitor 类

底层操作:

  • 基于 SyncBlock 实现,SyncBlock 是 CLR 为每个对象分配的一个内部数据结构。

  • 使用 Monitor.Enter 和 Monitor.Exit 方法获取和释放锁。

  • 支持 WaitPulse 和 PulseAll 方法,用于线程间的信号通知。

特点:

  • 比 lock 更灵活,支持复杂的同步逻辑。

  • 可能会引入死锁问题。

  • 性能较低,因为涉及内核对象的切换。

适用场景:

  • 需要复杂同步逻辑的场景(如条件等待)。与lock类似,但提供了更多的控制,如TryEnterWaitPulse等。

private static readonly object _lock = new object();
public static void DoWork(){    Monitor.Enter(_lock);    try    {        // 临界区代码    }    finally    {        Monitor.Exit(_lock);    }}

 

4. SpinLock 结构

底层操作:

  • 使用自旋(spin)机制,线程在获取锁失败时会不断重试,而不是立即进入等待状态。

  • 基于 CPU 的原子操作实现。

特点:

  • 适用于短时间的等待,避免线程上下文切换的开销。

  • 长时间等待会浪费 CPU 资源。

  • 轻量级,性能高。

适用场景:

  • 短时间的临界区保护,锁竞争不激烈的场景。适用于短时间的等待,避免线程上下文切换的开销。

private static SpinLock _spinLock = new SpinLock();
public static void DoWork(){    bool lockTaken = false;    try    {        _spinLock.Enter(ref lockTaken);        // 临界区代码    }    finally    {        if (lockTaken)            _spinLock.Exit();    }}

 

5. WaitHandle 类

底层操作:

  • 基于内核对象(如事件、信号量、互斥体)实现。

  • 使用 WaitOneWaitAll 和 WaitAny 方法等待信号。

特点:

  • 支持跨进程同步。

  • 性能较低,因为涉及内核模式的切换。

  • 适用于复杂的线程同步场景。

适用场景:

  • 线程间的信号通知,复杂的同步逻辑。用于线程间的信号通知,如等待某个事件发生。

private static EventWaitHandle _waitHandle = new AutoResetEvent(false);
public static void DoWork(){    _waitHandle.WaitOne(); // 等待信号    // 继续执行}
public static void Signal(){    _waitHandle.Set(); // 发送信号}

 

6. Mutex 类

底层操作:

  • 基于内核对象的互斥体实现。

  • 支持跨进程同步。

特点:

  • 重量级,性能较低。

  • 支持递归锁(同一线程可以多次获取锁)。

  • 适用于跨进程的同步。

适用场景:

  • 跨进程的线程同步。

private static Mutex _mutex = new Mutex();
public static void DoWork(){    _mutex.WaitOne();    try    {        // 临界区代码    }    finally    {        _mutex.ReleaseMutex();    }}

 

7. Semaphore 类

底层操作:

  • 基于内核对象的信号量实现。

  • 使用 WaitOne 和 Release 方法控制资源的访问。

特点:

  • 允许多个线程同时访问资源,但数量有限。

  • 支持跨进程同步。

  • 性能较低,因为涉及内核模式的切换。

适用场景:

  • 限制资源访问的场景(如连接池、线程池)。

private static Semaphore _semaphore = new Semaphore(33); // 允许3个线程同时访问
public static void DoWork(){    _semaphore.WaitOne();    try    {        // 临界区代码    }    finally    {        _semaphore.Release();    }}

 

8. Events 类

底层操作:

  • 基于内核对象的事件实现。

  • 使用 Set 和 Reset 方法控制信号状态。

特点:

  • 支持手动重置和自动重置事件。

  • 适用于线程间的信号通知。

  • 性能较低,因为涉及内核模式的切换。

适用场景:

  • 线程间的信号通知,复杂的同步逻辑。用于线程间的信号通知,类似于WaitHandle,但更灵活。

private static ManualResetEventSlim _event = new ManualResetEventSlim(false);
public static void DoWork(){    _event.Wait(); // 等待信号    // 继续执行}
public static void Signal(){    _event.Set(); // 发送信号}

 

9. Barrier 类

底层操作:

  • 使用 SpinWait 和内核事件结合实现。

  • 允许多个线程在某个点同步,等待所有线程到达后再继续执行。

特点:

  • 适用于分阶段的并行任务。

  • 性能较高,因为大部分时间使用自旋等待。

适用场景:

  • 多个线程在某个点同步的场景(如并行计算)。用于多个线程在某个点同步,等待所有线程到达后再继续执行。

private static Barrier _barrier = new Barrier(3); // 等待3个线程
public static void DoWork(){    // 执行一些工作    _barrier.SignalAndWait(); // 等待其他线程    // 继续执行}
总结Barrier 适用于需要多个线程在某个点同步的场景,确保所有线程都到达后再继续执行。

 

10. ReaderWriterLockSlim 类

底层操作:

  • 使用自旋和内核事件结合实现。

  • 支持读写分离,允许多个读线程同时访问,但写线程独占访问。

特点:

  • 适用于读写分离的场景。

  • 性能较高,因为读操作可以并发执行。

  • 支持递归锁。

适用场景:

  • 读写分离的同步场景(如缓存、配置文件)。用于读写分离的同步场景,允许多个读线程同时访问,但写线程独占访问。

private static ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();
public static void Read(){    _rwLock.EnterReadLock();    try    {        // 读取操作    }    finally    {        _rwLock.ExitReadLock();    }}
public static void Write(){    _rwLock.EnterWriteLock();    try    {        // 写入操作    }    finally    {        _rwLock.ExitWriteLock();    }}

总结ReaderWriterLockSlim 适用于读写分离的场景,提高了读操作的并发性能。

 


 

总结

  • lock:简单易用,适用于大多数同步场景。

  • Interlocked:高效的原子操作,适用于简单的数值操作。

  • Monitor:提供更细粒度的控制,适用于复杂同步逻辑。

  • SpinLock:适用于短时间的等待,避免线程上下文切换。

  • WaitHandle:线程间的信号通知,适用于复杂同步场景。

  • Mutex:跨进程的同步,性能较低。

  • Semaphore:限制资源访问,允许多个线程同时访问。

  • Events:灵活的线程间信号通知机制。

  • Barrier:多个线程在某个点同步。

  • ReaderWriterLockSlim:读写分离的同步场景,提高读操作的并发性能。

根据具体场景选择合适的同步机制,可以提高程序的性能和可靠性。

 

总结对比

同步机制底层实现性能适用场景跨进程支持递归锁支持
lock 基于 Monitor 中等 简单的临界区保护 不支持 支持
Interlocked CPU 原子指令 简单的原子操作 不支持 不支持
Monitor 基于 SyncBlock 中等 复杂的同步逻辑 不支持 支持
SpinLock 自旋等待 短时间的临界区保护 不支持 不支持
WaitHandle 内核对象(事件、信号量等) 线程间的信号通知 支持 不支持
Mutex 内核对象(互斥体) 跨进程的线程同步 支持 支持
Semaphore 内核对象(信号量) 限制资源访问 支持 不支持
Events 内核对象(事件) 线程间的信号通知 支持 不支持
Barrier 自旋等待 + 内核事件 多个线程在某个点同步 不支持 不支持
ReaderWriterLockSlim 自旋等待 + 内核事件 读写分离的同步场景 不支持 支持

选择建议:

  • 高性能场景:优先选择 InterlockedSpinLock 或 ReaderWriterLockSlim

  • 简单同步:使用 lock 或 Monitor

  • 复杂同步:使用 WaitHandleEvents 或 Barrier

  • 跨进程同步:使用 Mutex 或 Semaphore

 

Copyright © 2000-2022 Lzhdim Technology Software All Rights Reserved