C# 多线程锁
分类
- lock (Monitor):
- lock 是 C# 中的关键字,它实际上是 Monitor 类的一个简化版本的语法糖。
- 使用方式:lock (obj) { // 代码块 },其中 obj 是一个对象引用,所有线程都试图获取该对象的互斥锁。
- 功能:确保同一时间只有一个线程可以进入受保护的代码块。
- 应用场景:适用于大多数简单的互斥需求,尤其是在对某个数据结构或对象进行修改时,需要保证操作的原子性和一致性。
- Monitor:
- Monitor 类提供了比 lock 更底层的方法,如 Monitor.Enter(obj) 和 Monitor.Exit(obj),以及 Monitor.TryEnter(obj, timeout),Wait,Pulse和PulseAll 等高级功能。
- 包括了锁的获取和释放,并支持条件变量,允许线程等待特定条件变为真后继续执行。
- 应用场景:与 lock 类似,但在需要更精细控制的情况下,比如手动控制锁的获取和释放,或者在等待条件满足时才继续执行。
- Mutex:
- Mutex 是操作系统级别的互斥体,跨进程有效。
- 可以在多个进程间同步访问资源。
- 使用 WaitOne() 和 ReleaseMutex() 方法来获取和释放锁。
- 应用场景:跨进程的同步,尤其是在多个应用程序之间需要共享资源或协调操作时。
- Semaphore:
- 信号量允许一定数量的线程同时访问特定资源。
- 通过控制可同时进入关键区域的线程数来管理资源池。
- 应用场景:当有多个类似资源但不希望所有线程同时访问时,如数据库连接池等。
- 轻量级版为SemaphoreSlim,支持异步。
- ReaderWriterLockSlim:
- 提供了读写锁的功能,允许多个读取者共享锁,但在写入者请求时阻止所有读取和写入。
- 提高了读取密集型场景下的并发性能。
- 使用 EnterReadLock(), ExitReadLock(), EnterWriteLock(), ExitWriteLock() 等方法。
- 应用场景:适用于读取频繁而写入较少的数据结构,例如缓存或配置信息。
- 重量级版为ReaderWriterLock。
- SpinLock:
- 自旋锁是一种低级别的锁机制,在尝试获取锁时,线程不会立即挂起,而是循环检查锁的状态(即“自旋”)直至锁可用。
- 适合于锁持有时间极短且线程切换开销较大的场合。
- 使用 SpinLock.SpinUntil() 或 TryEnter() 方法。
- 应用场景:在预期锁很快会被释放的情况下,特别是在高性能计算环境中。
- volatile 关键字:
- 不是严格意义上的锁,但有助于确保多线程环境下对变量的访问可见性。
- 当标记一个字段为 volatile 后,编译器和运行时会确保任何对该字段的读/写操作都不会被优化掉,并且会从主内存而非CPU缓存中读取值。
- 应用场景:主要用于确保线程间的内存可见性,但它并不能保证原子操作。
区别
- Semaphore vs SemaphoreSlim
- Semaphore和SemaphoreSlim都是用于限制同时访问某一资源或资源池的线程数量的同步原语。它们都有一个计数器,当线程请求访问资源时,计数器会减少;当线程释放资源时,计数器会增加。当计数器为零时,新的线程将被阻塞,直到其他线程释放资源。
- Semaphore是一个重量级的同步原语,可以在不同的进程之间使用。它涉及到系统级的操作,所以使用Semaphore的开销比使用SemaphoreSlim更大。
- SemaphoreSlim是一个轻量级的同步原语,只能在同一进程的不同线程之间使用。它主要用于在任务和线程之间进行同步,特别是在并行程序中。
- ReaderWriterLock vs ReaderWriterLockSlim
- ReaderWriterLock和ReaderWriterLockSlim都是用于控制对资源的读写访问的同步原语。它们允许多个线程同时进行读取操作,但一次只允许一个线程进行写入操作。
- ReaderWriterLock是一个早期的.NET同步原语。尽管它在功能上很强大,但在性能上存在一些问题。特别是在高竞争的情况下,ReaderWriterLock可能会导致线程饥饿。
- ReaderWriterLockSlim是.NET 3.5引入的一个新的同步原语,用于解决ReaderWriterLock的一些性能问题。ReaderWriterLockSlim在设计时考虑了性能,因此在许多情况下,它比ReaderWriterLock更快。然而,ReaderWriterLockSlim的API更复杂,需要更谨慎的使用。
- 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()就可以完全释放锁。
注意: 跨进程锁,是指在同一操作系统下,比如一个应用程序的多开。
使用场景
- 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
,而不是创建一个新的。然后,这个进程可以通过WaitOne
和ReleaseMutex
方法来获取和释放Mutex
,从而实现对共享资源的同步访问。
需要注意的是,Mutex
是一个重量级的同步原语,因为它涉及到系统级的操作。因此,如果你只需要在同一进程的线程之间进行同步,你应该使用更轻量级的同步原语,如lock
或Monitor
。
- 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,锁真正被释放
}
}
标签:释放,同步,C#,lock,获取,线程,Mutex,多线程
From: https://www.cnblogs.com/Nine4Cool/p/18086756