管理

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

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

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)实现原子操作。

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

特点:

  • 轻量级,性能高。

  • 仅支持简单的原子操作(如 Increment、Decrement、Exchange、CompareExchange)。

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

适用场景:

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

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

 

3. Monitor 类

底层操作:

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

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

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

特点:

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

  • 可能会引入死锁问题。

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

适用场景:

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

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 类

底层操作:

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

  • 使用 WaitOne、WaitAll 和 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(3, 3); // 允许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 自旋等待 + 内核事件 高 读写分离的同步场景 不支持 支持

选择建议:

  • 高性能场景:优先选择 Interlocked、SpinLock 或 ReaderWriterLockSlim。

  • 简单同步:使用 lock 或 Monitor。

  • 复杂同步:使用 WaitHandle、Events 或 Barrier。

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

 

Copyright © 2000-2022 Lzhdim Technology Software All Rights Reserved