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