C#线程:非排他锁
非排他锁目的是限制并发性。
信号量
信号量(semaphore)就像俱乐部一样:它有特定的容量,还有门卫保护。在满员之后,就不允许其他人进入了,人们只能在外面排队。只有当有人离开时,才准许另外一个人进入。信号量的构造器需要至少两个参数:即俱乐部当前的空闲容量以及俱乐部的总容量。
容量为1的信号量与Mutex和lock类似,但是信号量没有持有者这个概念,它与线程无关。任何线程都可以调用Semaphore的Release方法。Mutex和lock则不然,只有持有锁的线程才能够释放锁。
信号量可用于限制并发性,防止太多线程同时执行特定的代码。
在下面的例子中,5个线程试图进入俱乐部,但最多只允许3个线程同时进入:
static SemaphoreSlim _sem = new SemaphoreSlim(3); static void Enter(object id) { Console.WriteLine(id + " wants to enter"); _sem.Wait(); Console.WriteLine(id + " is in!"); Thread.Sleep(1000 * (int)id); Console.WriteLine(id + " is leaving"); _sem.Release(); } static void Main(string[] args) { for (int i = 1; i <= 5; i++) new Thread(Enter).Start(i); Console.ReadKey(); } // 运行结果 1 wants to enter 4 wants to enter 2 wants to enter 3 wants to enter 5 wants to enter 4 is in! 2 is in! 1 is in! 1 is leaving 3 is in! 2 is leaving 5 is in! 3 is leaving 4 is leaving 5 is leaving
异步信号量与锁
在await语句周围使用锁是非法的:
lock(_locker) { await Task.Delay(1000); ... }
这是因为上述语句没有任何意义。锁是由线程持有的,而await返回时线程往往会发生变化。锁还会阻塞,而长时间的阻塞正是异步函数希望避免的情形。
但有时,我们仍期望异步函数按顺序执行,或至少限制其并发性,让不多于n个操作同时执行。例如,Web浏览器会并行执行异步的下载操作。但是它可能期望对此引入一些限制,例如同时执行的下载操作不得超过10个。此时可以使用SemaphoreSlim来实现该功能:
SemaphoreSlim _semaphore = new SemaphoreSlim(10); async Task<byte[]> DownloadWithSemaphoreAsync(string uri) { await _semaphore.WaitAsync(); try { return await new WebClient().DownloadDataTaskAsync(uri); } finally { _semaphore.Release(); } }
将信号量的initialCount设置为1会将最大并行数目设置为1,即它会变为一个异步锁。
读写锁
通常,一个类型实例的并发读操作是线程安全的,其并发更新操作则不然。虽然可以简单地使用一个排他锁来保护对实例的任何形式的访问,但是如果其读操作很多但更新操作很少,则使用单一的锁限制并发性就不太合理了。这种情况常出现在业务应用服务器上,它会将常用的数据缓存在静态字段中进行快速检索。ReaderWriterLockSlim是专门为这种情形而设计的,它可以最大限度地保证锁的可用性。
ReaderWriterLockSlim
和ReaderWriterLock
都拥有两种基本的锁,即读锁和写锁:
- 写锁是全局排他锁。
- 读锁可以兼容其他的读锁。
因此,持有写锁的线程将阻塞其他任何试图获取读锁或写锁的线程(反之亦然)。但是如果没有任何线程持有写锁的话,其他任意数量的线程都可以并发获得读锁。ReaderWriterLockSlim类定义了以下的方法来获得和释放读写锁:
public void EnterReadLock(); public void ExitReadLock(); public void EnterWriteLock(); public void ExitWriteLock();
以下示例演示了ReaderWriterLockSlim的用法。三个线程将持续枚举列表中的元素,另有两个线程则会每隔100毫秒生成一个随机数,并试图将该数字加入列表中。其中,读锁保护列表的读操作,写锁保护列表的写操作:
static ReaderWriterLockSlim _rw = new ReaderWriterLockSlim(); static List<int> _items = new List<int>(); static Random _rand = new Random(); static void Main(string[] args) { new Thread(Read).Start(); new Thread(Read).Start(); new Thread(Read).Start(); new Thread(Write).Start("A"); new Thread(Write).Start("B"); Console.ReadKey(); } static void Read() { while (true) { _rw.EnterReadLock(); foreach (int i in _items) Thread.Sleep(10); _rw.ExitReadLock(); } } static void Write(object threadID) { while (true) { int newNumber = GetRandNum(100); _rw.EnterWriteLock(); _items.Add(newNumber); _rw.ExitWriteLock(); Console.WriteLine("线程 " + threadID + " 添加: " + newNumber); Thread.Sleep(100); } } static int GetRandNum(int max) { lock (_rand) return _rand.Next(max); }
ReaderWriterLockSlim相比简单的锁能提供更多的并发Read操作。如:_rw.CurrentReadCount就能得到当前读锁的并发个数,上例是3个。除了这个,还提供了其他监视锁状态的属性。
可升级锁
有时最好能在一个原子操作中将读锁转换为写锁。例如,我们希望当列表不包含特定元素时才将这个元素添加到列表中。理想情况下,我们希望尽可能缩短持有写锁(排他锁)的时间,假设可以采取如下的操作步骤:
- 获取一个读锁。
- 判断该元素是否已经位于列表中,如果确已存在,则释放读锁并返回。
- 释放读锁。
- 获得写锁。
- 添加该元素。
上述操作的问题在于,另一个线程可能会在第3和第4步之间插入并修改列表(例如,添加同一个元素)。而ReaderWriterLockSlim可以通过第三种锁来解决这个问题,该锁称为可升级锁。可升级锁就像读锁一样,但是它可以在随后通过一个原子操作升级为写锁。以下是其使用方式:
- 调用EnterUpgradeableReadLock。
- 执行读操作(例如,判断该元素是否已经存在于列表中)。
- 调用EnterWriteLock(该操作将可升级锁转化为写锁)。
- 执行写操作(例如,将该元素添加到列表中)。
- 调用ExitWriteLock(将写锁转换回可升级锁)。
- 执行其他读操作。
- 调用ExitUpgradeableReadLock。
从调用者的角度来看,这种操作和嵌套锁或者递归锁很相似。但是,从功能上,第3步中ReaderWriterLockSlim释放读锁并获得写锁的操作是原子的。
可升级锁和读锁还有一个重要区别:虽然可升级锁可以和任意数目的读锁并存,但是一次只能获取一个可升级锁。
我们对前一个例子的Write方法稍做修改来演示可升级锁的用法。这次仅在列表中不存在相应数字时才会将数字添加到列表中:
static void Write (object threadID) { while (true) { int newNumber = GetRandNum (100); _rw.EnterUpgradeableReadLock(); if (!_items.Contains (newNumber)) { _rw.EnterWriteLock(); _items.Add (newNumber); _rw.ExitWriteLock(); Console.WriteLine ("Thread " + threadID + " added " + newNumber); } _rw.ExitUpgradeableReadLock(); Thread.Sleep (100); } }
本文来自博客园,作者:一纸年华,转载请注明原文链接:https://www.cnblogs.com/nullcodeworld/p/16643150.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?