ReaderWriterLockSlim和async\await

l0n*_*ley 26 c# asynchronous locking

我有一些问题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>
    }
Run Code Online (Sandbox Code Playgroud)

当我在输入写锁定<1>,在调试器我可以看到_indexLock.IsWriteLockHeldtrue,但是当执行步骤<2>我看_indexLock.IsWriteLockHeldfalse_indexLock.ExitWriteLock抛出异常SynchronizationLockException处理消息"写锁定而不被保持被释放".我做错了什么?

Ste*_*ary 44

ReaderWriterLockSlim是一个线程仿射锁类型,因此它通常不能与async和一起使用await.

您应该使用SemaphoreSlimWaitAsync,或者(如果你真的需要一个读/写器锁),用我AsyncReaderWriterLock从AsyncEx斯蒂芬Toub的AsyncReaderWriterLock.

  • @DonBox:您在评论中提出了很多问题。我认为提出自己的问题会对您有所帮助。 (2认同)

myc*_*elo 6

您可以使用可靠且轻量级的方法安全地模拟读取器/写入器锁定机制,并保留/SemaphoreSlim的优点。创建一个可用锁的数量,该锁的数量等于锁定资源以同时读取的例程数量。每个人都会像往常一样请求一把锁。对于您的写入例程,请确保它在执行操作之前请求所有可用的锁。这样,您的写作例程将始终单独运行,而您的阅读例程可能仅在它们之间共享资源。例如,假设您有 2 个阅读例程和 1 个写作例程。asyncawaitSemaphoreSlim



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)
    }
}
Run Code Online (Sandbox Code Playgroud)

显然,只有当您事先知道可以同时运行多少个阅读例程时,此方法才有效。不可否认,太多会使这段代码变得非常难看。

  • 几个月后再看:如果两个线程同时调用“ExclusiveWriter”,是否有可能出现死锁?如果两者都通过了第一个“WaitAsync()”调用,则调用计数将增加到两次,并且两者都将等待“Release()”。 (16认同)
  • 这不提供读/写语义。读取器/写入器锁的想法是,可以有任意数量的并发读取器,但当正在进行写入操作时,不能有任何读取器或其他写入器。这既不必要地限制了读取器,也没有在一个操作正在写入时正确限制读取器或其他写入器。 (4认同)
  • 当您有活跃的读者时,我确实会阻止任何作家,并且当您没有任何活跃的作家时,它仍然允许并发读者。这本质上就是读取器/写入器锁应该做的事情,除非您不知道您有多少个读取器,正如我**明确**指出的那样(如果您认真考虑的话,甚至可以解决这个问题,但是我不会破坏你的惊喜)。对于您的标准来说,它也可能不是那么优雅,但它**是**一个有效的解决方案,我自己在经过充分测试、压力很大的高需求服务中使用了它。我总是想变得简单,但每个人都有自己的... (2认同)
  • @MarcL。我已将独占写入方法包装在其自己的 writeLock 信号量中。只允许内部有一个线程。 (2认同)

xta*_*dex 5

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

var rwLock = new AsyncReaderWriterLock();

await rwLock.AcquireReaderLock();
try
{
    // ... reading ...
}
finally
{
    rwLock.ReleaseReaderLock();
}

await rwLock.AcquireWriterLock();
try
{
    // ... writing ...
}
finally
{
    rwLock.ReleaseWriterLock();
}


Run Code Online (Sandbox Code Playgroud)
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();
    }
}
Run Code Online (Sandbox Code Playgroud)

  • @TheodorZoulias,您对 AcquireReaderLock 首先获取 _writeSemaphore 以确保此时没有任何写入器在起作用。一旦获得_readerLock,写入信号量立即释放。如果您注意到,读/写获取方法会等待两个信号量。因此,SafeAcquireReadSemaphore() 用于在第二个 WaitAsync() 中发生取消的情况,以便在 OCE 情况下正确释放资源。 (2认同)
  • @xtadex 没有找到其他任何内容,这是我使用的版本,根据我的口味进行了轻微重构和一些注释 https://github.com/copenhagenatomics/CA_DataUploader/pull/90/files#diff-24a9664c904fe9276878f37dc1438aae578a76b7ef34eabbebf6ac66eaad83e6 ..谢谢! (2认同)
  • 这个版本与我创建的用于验证的单元测试配合得很好,而 Alexander 的最新(并且看起来更简单)版本却因死锁而失败了相同的测试。但我真的很喜欢“IDisposable”提供的“using() {}”符号,因此我改编了@eglasius的版本来支持它:https://gist.github.com/cajuncoding/a88f0d00847dcfc241ae80d1c7bafb1e (2认同)