目录
在C#语言中,为了在多线程环境中保护共享资源,防止线程间资源竞争,引入了多种锁的机制
本文将介绍一些常见的C#的锁以及其作用,概念和使用方式
一.C#中存在的锁
- lock关键字(基于Monitor):这是C#中最简单和常用的同步机制,用于确保在给定的对象上,同一时间只有一个线程可以进入被锁定的代码块
- Monitor类:提供了静态方法Enter和Exit,与lock关键字功能类似,但提供了更细粒度的控制,例如尝试进入和等待通知等
- Mutex(互斥量):系统级别的同步机制,可在跨进程之间同步进程,对比lock和Monitor,Mutex可以用于同步不同进程中的线程
- Semaphore和SemaphoreSlim:信号量允许限定一定数量的线程同时访问一个资源,SemaphoreSlim是轻量级版本,主要用于进程内同步
- ReaderWriterLockSlim:允许多个线程同时读取资源,但在写入时,要求独占访问.适用于读多写少的场景,以提高并发性能
- SpinLock:一种自旋锁,用于短时间的锁定,可以减少线程上下文切换的开销,但是需要去谨慎的去使用,避免占用CPU时间过长
- Interlocked类:提供了一组原子操作,能够在不使用锁的情况下对共享变量进行线程安全的操作,例如增减,交换,比较交换等
- Task并行库中的常用机制:如CannellationToken,Barrier,CountdownEvent等,用于更高层次的任务同步
二.锁的作用
- 线程同步: 确保多个线程在访问共享资源时,不会产生数据不一致或者是竞态条件
- 保证数据的完整性:通过控制对资源的并发访问,防止数据损坏或者出现不可预期的结果
- 管理线程执行顺序:在复杂的多线程应用中,控制线程的执行顺序,确保程序按照预期的逻辑运行
三.锁的概念和定义
- 锁(Lock):是一种同步机制,用于确保在同一时间,只有一个线程可以访问某一资源或者执行某段代码,锁可以防止多个线程共同修改共享数据,导致数据不一致.
- 互斥(Mutual Exclusion):指在多线程环境中,确保只有一个线程能够访问共享资源的特性.
- 临界区(Critical Section):程序中访问共享资源的代码块,需要被保护以防止并发访问.
- 死锁(Deadlock):当多个线程互相等待对方释放资源时,导致程序无法继续执行的状态.
关于锁的完整代码示例
// 共享资源
private static int _sharedResource = 0;
private static List<int> _sharedList = new List<int>();
// 各种锁的对象声明
private static readonly object _lockObject = new object();
private static readonly object _monitorLock = new object();
private static readonly Mutex _mutex = new Mutex();
private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(2); // 允许2个并发访问
private static readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();
private static SpinLock _spinLock = new SpinLock();
static async Task Main(string[] args)
{
// 1. Lock示例
await DemonstrateLock();
// 2. Monitor示例
await DemonstrateMonitor();
// 3. Mutex示例
await DemonstrateMutex();
// 4. Semaphore示例
await DemonstrateSemaphore();
// 5. ReaderWriterLock示例
await DemonstrateReaderWriterLock();
// 6. SpinLock示例
await DemonstrateSpinLock();
// 7. Interlocked示例
DemonstrateInterlocked();
// 8. 任务并行库同步示例
await DemonstrateTPLSync();
}
// 1. Lock关键字示例
private static async Task DemonstrateLock()
{
Console.WriteLine("\n=== Lock示例 ===");
var tasks = new List<Task>();
for (int i = 0; i < 3; i++)
{
tasks.Add(Task.Run(() =>
{
// lock块确保同一时间只有一个线程可以执行这段代码
lock (_lockObject)
{
_sharedResource++;
Console.WriteLine($"Lock: 线程 {Task.CurrentId} 修改资源值为: {_sharedResource}");
Thread.Sleep(100); // 模拟工作
}
}));
}
await Task.WhenAll(tasks);
}
// Monitor类示例
private static async Task DemonstrateMonitor()
{
Console.WriteLine("\n=== Monitor示例 ===");
var tasks = new List<Task>();
for (int i = 0; i < 3; i++)
{
tasks.Add(Task.Run(() =>
{
bool lockTaken = false;
try
{
// 尝试获取锁,设置1秒超时
Monitor.TryEnter(_monitorLock, TimeSpan.FromSeconds(1), ref lockTaken);
if (lockTaken)
{
_sharedResource++;
Console.WriteLine($"Monitor: 线程 {Task.CurrentId} 修改资源值为: {_sharedResource}");
Thread.Sleep(100);
}
else
{
Console.WriteLine($"Monitor: 线程 {Task.CurrentId} 未能获取锁");
}
}
finally
{
// 如果成功获取了锁,则释放
if (lockTaken)
{
Monitor.Exit(_monitorLock);
}
}
}));
}
await Task.WhenAll(tasks);
}
// 3. Mutex示例(支持跨进程同步)
private static async Task DemonstrateMutex()
{
Console.WriteLine("\n=== Mutex示例 ===");
var tasks = new List<Task>();
// 创建一个命名互斥锁,可以跨进程使用
using (var namedMutex = new Mutex(false, "GlobalMutexExample"))
{
for (int i = 0; i < 3; i++)
{
tasks.Add(Task.Run(() =>
{
try
{
namedMutex.WaitOne();
_sharedResource++;
Console.WriteLine($"Mutex: 线程 {Task.CurrentId} 修改资源值为: {_sharedResource}");
Thread.Sleep(100);
}
finally
{
namedMutex.ReleaseMutex();
}
}));
}
await Task.WhenAll(tasks);
}
}
// 4. SemaphoreSlim示例
private static async Task DemonstrateSemaphore()
{
Console.WriteLine("\n=== SemaphoreSlim示例 ===");
var tasks = new List<Task>();
// 创建4个任务,但同时只允许2个任务执行
for (int i = 0; i < 4; i++)
{
tasks.Add(Task.Run(async () =>
{
await _semaphore.WaitAsync();
try
{
Console.WriteLine($"Semaphore: 线程 {Task.CurrentId} 开始执行");
await Task.Delay(500);
}
finally
{
_semaphore.Release();
Console.WriteLine($"Semaphore: 线程 {Task.CurrentId} 释放信号量");
}
}));
}
await Task.WhenAll(tasks);
}
// 5. ReaderWriterLockSlim示例
private static async Task DemonstrateReaderWriterLock()
{
Console.WriteLine("\n=== ReaderWriterLockSlim示例 ===");
var tasks = new List<Task>();
// 添加多个读取任务
for (int i = 0; i < 3; i++)
{
tasks.Add(Task.Run(() =>
{
_rwLock.EnterReadLock();
try
{
Console.WriteLine($"Reader: 线程 {Task.CurrentId} 正在读取");
Thread.Sleep(100);
}
finally
{
_rwLock.ExitReadLock();
}
}));
}
// 添加写入任务
tasks.Add(Task.Run(() =>
{
_rwLock.EnterWriteLock();
try
{
Console.WriteLine($"Writer: 线程 {Task.CurrentId} 正在写入");
Thread.Sleep(200);
}
finally
{
_rwLock.ExitWriteLock();
}
}));
await Task.WhenAll(tasks);
}
// 6. SpinLock示例
private static async Task DemonstrateSpinLock()
{
Console.WriteLine("\n=== SpinLock示例 ===");
var tasks = new List<Task>();
for (int i = 0; i < 3; i++)
{
tasks.Add(Task.Run(() =>
{
bool lockTaken = false;
try
{
_spinLock.Enter(ref lockTaken);
_sharedResource++;
Console.WriteLine($"SpinLock: 线程 {Task.CurrentId} 修改资源值为: {_sharedResource}");
// SpinLock适用于非常短的操作,这里仅作演示
}
finally
{
if (lockTaken) _spinLock.Exit();
}
}));
}
await Task.WhenAll(tasks);
}
// 7. Interlocked示例
private static void DemonstrateInterlocked()
{
Console.WriteLine("\n=== Interlocked示例 ===");
// 原子递增操作
int value = Interlocked.Increment(ref _sharedResource);
Console.WriteLine($"Interlocked递增后的值: {value}");
// 原子交换操作
int original = Interlocked.Exchange(ref _sharedResource, 100);
Console.WriteLine($"交换前的值: {original}, 交换后的值: {_sharedResource}");
// 比较并交换操作
int comparand = 100;
int newValue = 200;
int result = Interlocked.CompareExchange(ref _sharedResource, newValue, comparand);
Console.WriteLine($"比较交换结果: {result}, 当前值: {_sharedResource}");
}
// 8. 任务并行库同步示例
private static async Task DemonstrateTPLSync()
{
Console.WriteLine("\n=== TPL同步示例 ===");
// 使用CancellationTokenSource来控制任务取消
using (var cts = new CancellationTokenSource())
{
// 创建一个Barrier,等待3个任务都完成某个阶段
var barrier = new Barrier(3);
var tasks = new List<Task>();
for (int i = 0; i < 3; i++)
{
tasks.Add(Task.Run(() =>
{
Console.WriteLine($"任务 {Task.CurrentId} 开始第一阶段");
barrier.SignalAndWait(); // 等待所有任务完成第一阶段
Console.WriteLine($"任务 {Task.CurrentId} 开始第二阶段");
barrier.SignalAndWait(); // 等待所有任务完成第二阶段
}, cts.Token));
}
await Task.WhenAll(tasks);
}
Console.ReadKey();
}
代码逐层剖析:
全局变量与同步变量
// 共享资源
private static int _sharedResource = 0;
private static List<int> _sharedList = new List<int>();
// 各种锁的对象声明
private static readonly object _lockObject = new object();
private static readonly object _monitorLock = new object();
private static readonly Mutex _mutex = new Mutex();
private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(2); // 允许2个并发访问
private static readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();
private static SpinLock _spinLock = new SpinLock();
_sharedResource:这是一个整数,作为共享的资源,多个线程会对其进行读写
_sharedList:一个整数列表,用于示例需要
_lockObject等同步对象:这些对象用于各自的同步原语,以确保线程安全
Lock(锁)关键字示例
private static async Task DemonstrateLock()
{
Console.WriteLine("\n=== Lock示例 ===");
var tasks = new List<Task>();
for (int i = 0; i < 3; i++)
{
tasks.Add(Task.Run(() =>
{
// lock块确保同一时间只有一个线程可以执行这段代码
lock (_lockObject)
{
_sharedResource++;
Console.WriteLine($"Lock: 线程 {Task.CurrentId} 修改资源值为: {_sharedResource}");
Thread.Sleep(100); // 模拟工作
Console.WriteLine($"Lock: 线程 {Task.CurrentId} 释放锁");
}
}));
}
await Task.WhenAll(tasks);
}
代码解释:
- 目的:演示了lock关键字的使用,即如何确保在一个代码块内同时只能有一个线程运行
- 实现细节:
- 创建了三个任务,每个任务都尝试访问共享资源 _sharedResource
- 使用lock(_lockObject)来锁定代码块,这样同一时间只有一个线程能够进入该代码块
- 在锁定的代码块内,递增_sharedResource的值并输出当前线程的信息
- 重要性:lock是C#中最基本的同步机制,使用简单,但只能用于同一进程内的线程同步
代码运行结果展示
Monitor(监视器锁)示例
private static async Task DemonstrateMonitor()
{
Console.WriteLine("\n=== Monitor示例 ===");
var tasks = new List<Task>();
for (int i = 0; i < 3; i++)
{
tasks.Add(Task.Run(() =>
{
bool lockTaken = false;
try
{
// 尝试获取锁,设置1秒超时
Monitor.TryEnter(_monitorLock, TimeSpan.FromSeconds(1), ref lockTaken);
if (lockTaken)
{
_sharedResource++;
Console.WriteLine($"Monitor: 线程 {Task.CurrentId} 修改资源值为: {_sharedResource}");
Thread.Sleep(100);
}
else
{
Console.WriteLine($"Monitor: 线程 {Task.CurrentId} 未能获取锁");
}
}
finally
{
// 如果成功获取了锁,则释放
if (lockTaken)
{
Console.WriteLine($"Monitor: 线程 {Task.CurrentId} 释放锁");
Monitor.Exit(_monitorLock);
}
}
}));
}
await Task.WhenAll(tasks);
}
代码解释:
- 目的:演示Monitor类的使用,特别是TryEnter方法,以及如何手动管理锁的获取和释放
- 实现细节:
- 创建了三个任务,每个任务尝试获取_monitorLock的锁
- 使用Monitor.TryEnter方法,设置了一秒的超时时间,如果在这段时间内未能获取到锁,则返回False
- lockTaken是一个布尔量,表示是否成功获取到锁
- 如果成功获取了锁,执行共享资源的操作;否则输出未能获取锁的信息
- 在Finally块中,检查是否获取了锁,如果是,则调用Monitor.Exit释放锁
- 重要性:Monitor类提供了比lock更灵活的机制,比如超时等待,尝试获取锁等
代码运行结果展示:
Mutex(互斥量)示例(支持跨进程同步)
private static async Task DemonstrateMutex()
{
Console.WriteLine("\n=== Mutex示例 ===");
var tasks = new List<Task>();
// 创建一个命名互斥锁,可以跨进程使用
using (var namedMutex = new Mutex(false, "GlobalMutexExample"))
{
for (int i = 0; i < 3; i++)
{
tasks.Add(Task.Run(() =>
{
try
{
namedMutex.WaitOne();
_sharedResource++;
Console.WriteLine($"Mutex: 线程 {Task.CurrentId} 修改资源值为: {_sharedResource}");
Thread.Sleep(100);
}
finally
{
Console.WriteLine($"Mutex: 线程 {Task.CurrentId} 释放锁");
namedMutex.ReleaseMutex();
}
}));
}
await Task.WhenAll(tasks);
}
}
代码解释:
- 目的:演示Mutex(互斥量)的使用,特别是命名互斥量的跨进程同步特性
- 实现细节:
- 使用 new Mutex(false,"GlobalMutexExample")创建了一个命名的全局互斥锁,可以在不同的进程间共享
- 创建了3个任务,每个任务尝试进入互斥锁的临界区
- 使用 namedMutex.WaitOne()等待获取互斥锁
- 获取锁后,修改共享资源并输出信息
- 在finally块中,调用namedMutex.ReleaseMutex()释放互斥锁
- 重要性:Mutex可以用于跨进程的同步,这在需要多个进程访问同一资源时十分有用
代码运行结果展示:
SemaphoreSlim(信号量)示例
private static async Task DemonstrateSemaphore()
{
Console.WriteLine("\n=== SemaphoreSlim示例 ===");
var tasks = new List<Task>();
// 创建4个任务,但同时只允许2个任务执行
for (int i = 0; i < 4; i++)
{
tasks.Add(Task.Run(async () =>
{
await _semaphore.WaitAsync();
try
{
Console.WriteLine($"Semaphore: 线程 {Task.CurrentId} 开始执行");
await Task.Delay(500);
}
finally
{
_semaphore.Release();
Console.WriteLine($"Semaphore: 线程 {Task.CurrentId} 释放信号量");
}
}));
}
await Task.WhenAll(tasks);
}
代码解释:
- 目的:演示SemaphoreSlim的使用,控制同时访问某资源的线程数
- 实现细节:
- _semaphore初始化时设置了初始计数值为2,因此最多允许2个线程同时进入
- 创建了四个任务,每个任务都试图获取信号量
- 使用await _semaphore.WaitAsync() 异步地等待信号量
- 获取信号量后,输出信息并等待500毫秒,模拟工作
- 在finally块中,调用 _semaphore.Release() 方法去释放信号量,并输出信息
- 重要性:信号量用于限制同时访问某资源或代码块的线程数量,SemaphoreSlim是线程内的轻量级版本
代码运行展示:
ReadWriterLockSlim(读写锁)示例
private static async Task DemonstrateReaderWriterLock()
{
Console.WriteLine("\n=== ReaderWriterLockSlim示例 ===");
var tasks = new List<Task>();
// 添加多个读取任务
for (int i = 0; i < 3; i++)
{
tasks.Add(Task.Run(() =>
{
_rwLock.EnterReadLock();
try
{
Console.WriteLine($"Reader: 线程 {Task.CurrentId} 正在读取");
Thread.Sleep(100);
}
finally
{
_rwLock.ExitReadLock();
}
}));
}
// 添加写入任务
tasks.Add(Task.Run(() =>
{
_rwLock.EnterWriteLock();
try
{
Console.WriteLine($"Writer: 线程 {Task.CurrentId} 正在写入");
Thread.Sleep(200);
}
finally
{
_rwLock.ExitWriteLock();
}
}));
await Task.WhenAll(tasks);
}
代码解释:
- 目的:演示ReadWriterLockSlim的使用,允许多个线程同时读取,但写入时需要独占
- 实现细节:
- 创建了3个读取任务,使用_rwLock.EnterReadLock()获取读锁
- 创建了1个写入任务,使用_rwLock.EnterWriteLock()获取写锁
- 读取任务之间可以并发执行,但写入任务需要等待所有读锁释放后才能获取写锁
- 每个任务在完成后,调用相应的ExitReadLock()和ExitWriteLock()释放锁
- 重要性:读写锁提高了并发性能,适用于读多写少的情景
代码运行结果展示:
SpinLock(自旋锁)示例
private static async Task DemonstrateSpinLock()
{
Console.WriteLine("\n=== SpinLock示例 ===");
var tasks = new List<Task>();
for (int i = 0; i < 3; i++)
{
tasks.Add(Task.Run(() =>
{
bool lockTaken = false;
try
{
_spinLock.Enter(ref lockTaken);
_sharedResource++;
Console.WriteLine($"SpinLock: 线程 {Task.CurrentId} 修改资源值为: {_sharedResource}");
// SpinLock适用于非常短的操作,这里仅作演示
}
finally
{
if (lockTaken) _spinLock.Exit();
}
}));
}
await Task.WhenAll(tasks);
}
代码解释:
- 目的:演示了SpinLock的使用,它是一种自旋锁,适用于非常短时间的锁定操作
- 实现细节:
- 创建了三个任务,每个任务都尝试获取_spinLock的锁
- 使用_spinLock.Enter(ref lockTaken)来获取锁
- 获取锁后,修改共享资源并输出信息
- 在finally块中,检查lockTaken,如果获取了锁,则调用_spinLock.Exit()方法释放锁
- 重要性:SpinLock适用于非常短的临界区,因为它会一直循环等待直至获取锁,避免线程上下文的切换,但如果操作时间过长,会导致CPU占用过高
在使用自旋锁时,一定要去释放锁,否则会造成死锁
代码运行结果展示:
InterLocked示例
private static void DemonstrateInterlocked()
{
Console.WriteLine("\n=== Interlocked示例 ===");
// 原子递增操作
int value = Interlocked.Increment(ref _sharedResource);
Console.WriteLine($"Interlocked递增后的值: {value}");
// 原子交换操作
int original = Interlocked.Exchange(ref _sharedResource, 100);
Console.WriteLine($"交换前的值: {original}, 交换后的值: {_sharedResource}");
// 比较并交换操作
int comparand = 100;
int newValue = 200;
int result = Interlocked.CompareExchange(ref _sharedResource, newValue, comparand);
Console.WriteLine($"比较交换结果: {result}, 当前值: {_sharedResource}");
}
代码解释:
- 目的:演示InterLocked类的使用,用于执行原子操作,避免显示的锁定
- 实现细节:
- Interlocked.Increment:对共享变量执行原子递增操作,返回递增后的值
- Interlocked.Exchange:将共享变量设置为新值,并返回原始值
- Interlocked.CompareExchange:如果共享变量的值等于比较值,则将其设置为新值,并返回原始值
- 重要性:InterLocked提供了高效的线程安全操作,适用于简单的变量操作,而不需要使用锁
代码运行结果展示:
任务并行库(TPL)同步示例
private static async Task DemonstrateTPLSync()
{
Console.WriteLine("\n=== TPL同步示例 ===");
// 使用CancellationTokenSource来控制任务取消
using (var cts = new CancellationTokenSource())
{
// 创建一个Barrier,等待3个任务都完成某个阶段
var barrier = new Barrier(3);
var tasks = new List<Task>();
for (int i = 0; i < 3; i++)
{
tasks.Add(Task.Run(() =>
{
Console.WriteLine($"任务 {Task.CurrentId} 开始第一阶段");
barrier.SignalAndWait(); // 等待所有任务完成第一阶段
Console.WriteLine($"任务 {Task.CurrentId} 开始第二阶段");
barrier.SignalAndWait(); // 等待所有任务完成第二阶段
}, cts.Token));
}
await Task.WhenAll(tasks);
}
Console.ReadKey();
}
代码解释:
- 目的:演示任务并行库(TPL)中的同步机制,特别是Barrier的使用
- 实现细节:
- 创建了一个Barrier,参与方数量为3,表示需要等待三个任务
- 创建了3个任务,每个任务在两个阶段都调用了barrier.SignalAndWait();
- 第一阶段:任务输出开始第一阶段的信息,然后调用barrier.SignalAndWait(),等待其他任务也完成第一阶段
- 第二阶段:任务输出开始第二阶段的信息,然后再次调用barrier.SignalAndWait()
- CancellationTokenSource可以控制任务的取消
- 重要性:Barrier用于协调多个任务的执行,使得它们可以在某个同步点等待彼此,同步进入下一阶段的操作
代码运行结果展示:
总结
- lock和Monitor:适用于简单的线程同步,同一进程内的线程
- Mutex:可用于跨进程的同步,但性能较
lock
较低 - SemaphoreSlim:控制同时访问资源的线程数量
- ReadWriterLockSlim:优化读多写少的场景,提高并发性能
- SpinLock:适用于非常短的锁定操作,避免线程上下文切换,但可能导致CPU占用高
- InterLocked:提供简单的原子操作,性能高,没有锁开销
- 任务并行库同步机制""提供高级的同步方式,如
Barrier,CountdownEvent
等