ReaderWriterLockSlim and asyncawait

本文关键字:asyncawait and ReaderWriterLockSlim | 更新日期: 2023-09-27 18:37:13

我对ReaderWriterLockSlim有一些问题。我不明白它是如何神奇的。

我的代码:

 private async Task LoadIndex()
    {
        if (!File.Exists(FileName + ".index.txt"))
        {
            return;
        }
        _indexLock.EnterWriteLock();// <1>
        _index.Clear();
        using (TextReader index = File.OpenText(FileName + ".index.txt"))
        {
            string s;
            while (null != (s = await index.ReadLineAsync()))
            {
                var ss = s.Split(':');
                _index.Add(ss[0], Convert.ToInt64(ss[1]));
            }
        }
        _indexLock.ExitWriteLock();<2>
    }

当我在 <1> 处输入写锁定时,在调试器中我可以看到_indexLock.IsWriteLockHeld true,但是当执行步骤到 <2 时>我看到_indexLock.IsWriteLockHeldfalse并且_indexLock.ExitWriteLock抛出异常SynchronizationLockException消息"写锁定正在释放而未被保留"。我做错了什么?

ReaderWriterLockSlim and asyncawait

>ReaderWriterLockSlim是线程仿射锁类型,因此通常不能与asyncawait一起使用。

您应该将SemaphoreSlimWaitAsync一起使用,或者(如果您确实需要读取器/写入器锁),请使用我从AsyncEx或Stephen Toub的AsyncReaderWriterLock AsyncReaderWriterLock

您可以使用可靠且轻量级的SemaphoreSlim安全地模拟读/写器锁定机制,并保持async/await的好处。创建SemaphoreSlim为其提供可用锁数,这些锁数相当于将锁定资源以同时读取的例程数。每个人都会像往常一样请求一把锁。对于您的写作例程,请确保它在执行操作之前请求所有可用的锁。

这样,您的写作例程将始终单独运行,而您的阅读例程可能仅在它们之间共享资源。

例如,假设您有 2 个阅读例程和 1 个写作例程。

SemaphoreSlim semaphore = new SemaphoreSlim(2);
async void Reader1()
{
    await semaphore.WaitAsync();
    try
    {
        // ... reading stuff ...
    }
    finally
    {
        semaphore.Release();
    }
}
async void Reader2()
{
    await semaphore.WaitAsync();
    try
    {
        // ... reading other stuff ...
    }
    finally
    {
        semaphore.Release();
    }
}
async void ExclusiveWriter()
{
    // the exclusive writer must request all locks
    // to make sure the readers don't have any of them
    // (I wish we could specify the number of locks
    // instead of spamming multiple calls!)
    await semaphore.WaitAsync();
    await semaphore.WaitAsync();
    try
    {
        // ... writing stuff ...
    }
    finally
    {
        // release all locks here
        semaphore.Release(2);
        // (oh here we don't need multiple calls, how about that)
    }
}

显然,这种方法只有在您事先知道可以同时运行多少个阅读例程时才有效。诚然,太多会使这段代码变得非常丑陋。

前段时间,我为我的项目类实现了基于两个信号量Slim的项目类AsyncReaderWriterLock。希望它能有所帮助。它以相同的逻辑(多读取器和单写入器)实现,同时支持异步/等待模式。当然,它不支持递归,也没有防止错误用法的保护:

var rwLock = new AsyncReaderWriterLock();
await rwLock.AcquireReaderLock();
try
{
    // ... reading ...
}
finally
{
    rwLock.ReleaseReaderLock();
}
await rwLock.AcquireWriterLock();
try
{
    // ... writing ...
}
finally
{
    rwLock.ReleaseWriterLock();
}

public sealed class AsyncReaderWriterLock : IDisposable
{
    private readonly SemaphoreSlim _readSemaphore  = new SemaphoreSlim(1, 1);
    private readonly SemaphoreSlim _writeSemaphore = new SemaphoreSlim(1, 1);
    private          int           _readerCount;
    public async Task AcquireWriterLock(CancellationToken token = default)
    {
        await _writeSemaphore.WaitAsync(token).ConfigureAwait(false);
        await SafeAcquireReadSemaphore(token).ConfigureAwait(false);
    }
    public void ReleaseWriterLock()
    {
        _readSemaphore.Release();
        _writeSemaphore.Release();
    }
    public async Task AcquireReaderLock(CancellationToken token = default)
    {
        await _writeSemaphore.WaitAsync(token).ConfigureAwait(false);
        if (Interlocked.Increment(ref _readerCount) == 1)
        {
            try
            {
                await SafeAcquireReadSemaphore(token).ConfigureAwait(false);
            }
            catch
            {
                Interlocked.Decrement(ref _readerCount);
                throw;
            }
        }
        _writeSemaphore.Release();
    }
    public void ReleaseReaderLock()
    {
        if (Interlocked.Decrement(ref _readerCount) == 0)
        {
            _readSemaphore.Release();
        }
    }
    private async Task SafeAcquireReadSemaphore(CancellationToken token)
    {
        try
        {
            await _readSemaphore.WaitAsync(token).ConfigureAwait(false);
        }
        catch
        {
            _writeSemaphore.Release();
            throw;
        }
    }
    public void Dispose()
    {
        _writeSemaphore.Dispose();
        _readSemaphore.Dispose();
    }
}

https://learn.microsoft.com/en-us/dotnet/api/system.threading.readerwriterlockslim?view=net-5.0

从源头:

ReaderWriterLockSlim 管理线程亲和性;即每个线程对象必须进行自己的方法调用才能进入和退出锁定模式。不线程可以更改另一个线程的模式。

所以这里期望的行为。Async/await 不保证在同一线程中的延续,因此当您在一个线程中进入写锁定并尝试在另一个线程中退出时,您可能会捕获异常。

最好使用其他答案中的其他锁定机制,例如 SemaphoreSlim .

就像Stephen Cleary所说,ReaderWriterLockSlim是一种线程仿射锁类型,因此它通常不能与async和await一起使用。

<小时 />

您必须构建一种机制来避免读取器和写入器同时访问共享数据。此算法应遵循一些规则。

请求读取器锁时:

  • 是否有激活的编写器锁?
  • 有没有已经排队的东西?(我认为按请求顺序执行它很好)

请求写入器锁时:

  • 是否有激活的编写器锁?(因为编写器锁不应该并行执行)
  • 是否有任何读取器锁定处于活动状态?
  • 有没有已经排队的东西?

如果这些 creteria 中的任何一个回答是,则应排队执行并在稍后执行。您可以使用 TaskCompletionSources 继续awaits

完成任何读取器或写入器后,应评估队列并在可能的情况下继续执行项目。

<小时 />

例如(nuget):AsyncReaderWriterLock