C# 多线程锁

C# 多线程锁

分类

  1. lock (Monitor):
  • lock 是 C# 中的关键字,它实际上是 Monitor 类的一个简化版本的语法糖。
  • 使用方式:lock (obj) { // 代码块 },其中 obj 是一个对象引用,所有线程都试图获取该对象的互斥锁。
  • 功能:确保同一时间只有一个线程可以进入受保护的代码块。
  • 应用场景:适用于大多数简单的互斥需求,尤其是在对某个数据结构或对象进行修改时,需要保证操作的原子性和一致性。
  1. Monitor:
  • Monitor 类提供了比 lock 更底层的方法,如 Monitor.Enter(obj) 和 Monitor.Exit(obj),以及 Monitor.TryEnter(obj, timeout),Wait,Pulse和PulseAll 等高级功能。
  • 包括了锁的获取和释放,并支持条件变量,允许线程等待特定条件变为真后继续执行。
  • 应用场景:与 lock 类似,但在需要更精细控制的情况下,比如手动控制锁的获取和释放,或者在等待条件满足时才继续执行。
  1. Mutex:
  • Mutex 是操作系统级别的互斥体,跨进程有效。
  • 可以在多个进程间同步访问资源。
  • 使用 WaitOne() 和 ReleaseMutex() 方法来获取和释放锁。
  • 应用场景:跨进程的同步,尤其是在多个应用程序之间需要共享资源或协调操作时。
  1. Semaphore:
  • 信号量允许一定数量的线程同时访问特定资源。
  • 通过控制可同时进入关键区域的线程数来管理资源池。
  • 应用场景:当有多个类似资源但不希望所有线程同时访问时,如数据库连接池等。
  • 轻量级版为SemaphoreSlim,支持异步。
  1. ReaderWriterLockSlim:
  • 提供了读写锁的功能,允许多个读取者共享锁,但在写入者请求时阻止所有读取和写入。
  • 提高了读取密集型场景下的并发性能。
  • 使用 EnterReadLock(), ExitReadLock(), EnterWriteLock(), ExitWriteLock() 等方法。
  • 应用场景:适用于读取频繁而写入较少的数据结构,例如缓存或配置信息。
  • 重量级版为ReaderWriterLock。
  1. SpinLock:
  • 自旋锁是一种低级别的锁机制,在尝试获取锁时,线程不会立即挂起,而是循环检查锁的状态(即“自旋”)直至锁可用。
  • 适合于锁持有时间极短且线程切换开销较大的场合。
  • 使用 SpinLock.SpinUntil() 或 TryEnter() 方法。
  • 应用场景:在预期锁很快会被释放的情况下,特别是在高性能计算环境中。
  1. volatile 关键字:
  • 不是严格意义上的锁,但有助于确保多线程环境下对变量的访问可见性。
  • 当标记一个字段为 volatile 后,编译器和运行时会确保任何对该字段的读/写操作都不会被优化掉,并且会从主内存而非CPU缓存中读取值。
  • 应用场景:主要用于确保线程间的内存可见性,但它并不能保证原子操作。

区别

  1. Semaphore vs SemaphoreSlim
  • Semaphore和SemaphoreSlim都是用于限制同时访问某一资源或资源池的线程数量的同步原语。它们都有一个计数器,当线程请求访问资源时,计数器会减少;当线程释放资源时,计数器会增加。当计数器为零时,新的线程将被阻塞,直到其他线程释放资源。
  • Semaphore是一个重量级的同步原语,可以在不同的进程之间使用。它涉及到系统级的操作,所以使用Semaphore的开销比使用SemaphoreSlim更大。
  • SemaphoreSlim是一个轻量级的同步原语,只能在同一进程的不同线程之间使用。它主要用于在任务和线程之间进行同步,特别是在并行程序中。
  1. ReaderWriterLock vs ReaderWriterLockSlim
  • ReaderWriterLock和ReaderWriterLockSlim都是用于控制对资源的读写访问的同步原语。它们允许多个线程同时进行读取操作,但一次只允许一个线程进行写入操作。
  • ReaderWriterLock是一个早期的.NET同步原语。尽管它在功能上很强大,但在性能上存在一些问题。特别是在高竞争的情况下,ReaderWriterLock可能会导致线程饥饿。
  • ReaderWriterLockSlim是.NET 3.5引入的一个新的同步原语,用于解决ReaderWriterLock的一些性能问题。ReaderWriterLockSlim在设计时考虑了性能,因此在许多情况下,它比ReaderWriterLock更快。然而,ReaderWriterLockSlim的API更复杂,需要更谨慎的使用。
  1. lock vs Mutex
  • 作用范围lock只能在同一进程中的不同线程之间进行同步,而Mutex可以在不同的进程之间进行同步。这意味着如果你需要在多个应用程序之间同步访问某个资源,你应该使用Mutex
  • 性能:由于Mutex可以跨进程使用,因此它需要进行更多的系统级操作,这使得Mutex的性能开销比lock更大。如果你只需要在同一进程的线程之间进行同步,通常推荐使用lock,因为它的性能更好。
  • 所有权Mutex有所有权的概念,即只有创建或获取Mutex的线程才能释放它。而lock则没有这个限制,任何线程都可以释放lock
  • 异常安全:在lock的代码块中,如果发生异常,lock会自动释放。但是,如果你使用Mutex,你需要在finally块中显式释放它,以确保在发生异常时Mutex被正确释放。
  • 重入机制:lock(Monitor)和Mutex都允许重入,但二者有些区别:Mutex对于每次成功的WaitOne()调用(即获取锁),都需要对应一次ReleaseMutex()调用(即释放锁)。这意味着如果一个线程已经拥有了Mutex,然后再次尝试获取同一个Mutex,那么这个线程可以成功获取,但是这个线程需要调用两次ReleaseMutex()来完全释放Mutex。而lock(Monitor)不需要对应的多次Exit()调用。也就是说,无论Enter()调用了多少次,只需要一次Exit()就可以完全释放锁。

注意: 跨进程锁,是指在同一操作系统下,比如一个应用程序的多开。

使用场景

  1. Mutex
  • 跨进程实现

    Mutex(互斥锁)是一种同步原语,可以用于跨进程同步。在C#中,你可以通过命名Mutex来实现跨进程同步。

当你创建一个Mutex时,你可以给它一个唯一的名称。然后,其他的进程可以通过这个名称来打开并使用同一个Mutex。这样,不同的进程就可以通过这个共享的Mutex来同步对共享资源的访问。

以下是一个例子:

// 创建一个名为"MyMutex"的Mutex
bool createdNew;
Mutex mutex = new Mutex(true, "MyMutex", out createdNew);

if (createdNew)
{
    Console.WriteLine("This process created the mutex.");
}
else
{
    Console.WriteLine("This process opened an existing mutex.");
}

// 使用Mutex保护的代码区域
try
{
    // 获取Mutex
    mutex.WaitOne();

    // 在这里访问共享资源
}
finally
{
    // 释放Mutex
    mutex.ReleaseMutex();
}

在这个例子中,如果"MyMutex"已经存在,那么新的进程将打开已经存在的Mutex,而不是创建一个新的。然后,这个进程可以通过WaitOneReleaseMutex方法来获取和释放Mutex,从而实现对共享资源的同步访问。

需要注意的是,Mutex是一个重量级的同步原语,因为它涉及到系统级的操作。因此,如果你只需要在同一进程的线程之间进行同步,你应该使用更轻量级的同步原语,如lockMonitor

  • Mutex递归和非递归

非递归Mutex(Non-Recursive Mutex): 假设有一个非递归Mutex M,线程A先获取了M,此时其他线程无法获取M。如果线程A在未释放M之前再次尝试获取M,非递归Mutex会认为这是一个错误的行为(即发生了死锁),因为线程A已经持有这个锁,再次请求会导致线程A自己被阻塞。

using System.Threading;

class NonRecursiveMutexExample
{
    Mutex nonRecursiveMutex = new Mutex(false, "NonRecursiveMutex");

    public void SomeMethod()
    {
        nonRecursiveMutex.WaitOne(); // 线程A获取了mutex
        
        // ... 执行一些操作 ...
        
        // 如果在这儿再次尝试获取mutex
        nonRecursiveMutex.WaitOne(); // 此时线程A会被阻塞,因为它已经在等待自己持有的锁
    }
}

递归Mutex(Recursive Mutex): 对于递归Mutex,线程在已经持有锁的情况下可以再次获取该锁,并跟踪锁的获取次数。只有在释放相同次数后,锁才会真正被释放给其他线程。

using System.Threading;

class RecursiveMutexExample
{
    Mutex recursiveMutex = new Mutex(true, "RecursiveMutex"); // 注意这里的构造函数参数true表示创建递归Mutex

    public void SomeMethod()
    {
        recursiveMutex.WaitOne(); // 线程A获取了mutex
        
        // ... 执行一些操作 ...

        // 再次尝试获取mutex
        recursiveMutex.WaitOne(); // 由于是递归Mutex,线程A仍能获取,内部计数器加1
        // ... 继续执行一些依赖于锁的操作 ...

        // 要释放锁,需要调用ReleaseMutex对应次数
        recursiveMutex.ReleaseMutex(); // 第一次释放,计数器减1,锁仍然保持
        recursiveMutex.ReleaseMutex(); // 第二次释放,计数器减至0,锁真正被释放
    }
}
  1. lock避免使用如下结构锁
  • 锁定public对象
  • lock(this)
  • lock(typeof(MyType))
  • lock("mylock")
posted @ 2024-03-21 11:49  Nine4酷  阅读(145)  评论(0编辑  收藏  举报