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是专门为这种情形而设计的,它可以最大限度地保证锁的可用性。
ReaderWriterLockSlimReaderWriterLock都拥有两种基本的锁,即读锁和写锁:

  • 写锁是全局排他锁。
  • 读锁可以兼容其他的读锁。

因此,持有写锁的线程将阻塞其他任何试图获取读锁或写锁的线程(反之亦然)。但是如果没有任何线程持有写锁的话,其他任意数量的线程都可以并发获得读锁。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个。除了这个,还提供了其他监视锁状态的属性。

可升级锁

有时最好能在一个原子操作中将读锁转换为写锁。例如,我们希望当列表不包含特定元素时才将这个元素添加到列表中。理想情况下,我们希望尽可能缩短持有写锁(排他锁)的时间,假设可以采取如下的操作步骤:

  1. 获取一个读锁。
  2. 判断该元素是否已经位于列表中,如果确已存在,则释放读锁并返回。
  3. 释放读锁。
  4. 获得写锁。
  5. 添加该元素。

上述操作的问题在于,另一个线程可能会在第3和第4步之间插入并修改列表(例如,添加同一个元素)。而ReaderWriterLockSlim可以通过第三种锁来解决这个问题,该锁称为可升级锁。可升级锁就像读锁一样,但是它可以在随后通过一个原子操作升级为写锁。以下是其使用方式:

  1. 调用EnterUpgradeableReadLock
  2. 执行读操作(例如,判断该元素是否已经存在于列表中)。
  3. 调用EnterWriteLock(该操作将可升级锁转化为写锁)。
  4. 执行写操作(例如,将该元素添加到列表中)。
  5. 调用ExitWriteLock(将写锁转换回可升级锁)。
  6. 执行其他读操作。
  7. 调用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);
    }
}
posted @ 2022-08-31 15:03  一纸年华  阅读(107)  评论(0编辑  收藏  举报