使用 ConcurrentExclusiveSchedulerPair 作为异步 ReaderWriterLock 等价物

Using ConcurrentExclusiveSchedulerPair as an async ReaderWriterLock equivalent

我的应用程序有几个异步方法可以访问磁盘上的文件。当我了解到在这种情况下不能使用普通的 ReaderWriterLockSlim 时,我去寻找一个等价物。找到了看起来很有前途的 ConcurrentExclusiveSchedulerPair。我阅读了一些关于它的启发性博客文章,我开始认为这对我来说可能是正确的选择。

所以我将所有读取任务更改为使用并发调度程序,将写入任务更改为使用独占调度程序。但是,事实证明我仍然收到 IOExceptions 说文件正在使用中。这是我的代码的简化(但仍然失败)版本:

public async Task<IEnumerable<string>> Run()
{
    var schedulers = new ConcurrentExclusiveSchedulerPair();
    var exclusiveFactory = new TaskFactory(schedulers.ExclusiveScheduler);
    var concurrentFactory = new TaskFactory(schedulers.ConcurrentScheduler);

    var tasks = new List<Task>();
    for (var i = 0; i < 40; ++i)
    {
        // Create some readers and (less) writers
        if (i % 4 == 0)
        {
            var writeTask = exclusiveFactory.StartNew(WriteToFile).Unwrap();
            tasks.Add(writeTask);
        }
        else
        {
            var readTask = concurrentFactory.StartNew(ReadFromFile).Unwrap();
            tasks.Add(readTask);
        }
    }

    await Task.WhenAll(tasks);
    return _contents;
}

private async Task ReadFromFile()
{
    using (var fileStream = new FileStream("file.txt", FileMode.OpenOrCreate, FileAccess.Read))
    using (var reader = new StreamReader(fileStream))
    {
        await Task.Delay(500); // some other work
        _contents.Add(await reader.ReadToEndAsync());
    }
}

private async Task WriteToFile()
{
    using (var fileStream = new FileStream("file.txt", FileMode.OpenOrCreate, FileAccess.Write))
    using (var writer = new StreamWriter(fileStream))
    {
        await Task.Delay(500); // some other work
        await writer.WriteLineAsync("Lorem ipsum");
    }
}

然后我发现Stephen Cleary's blog post有红框警告:

When an asynchronous method awaits, it returns back to its context. This means that ExclusiveScheduler is perfectly happy to run one task at a time, not one task until it completes. As soon as an asynchronous method awaits, it’s no longer the “owner” of the ExclusiveScheduler. Stephen Toub’s async-friendly primitives like AsyncLock use a different strategy, allowing an asynchronous method to hold the lock while it awaits.

"One task at a time, not one task until it completes" - 现在这有点令人困惑。这是否意味着 ConcurrentExclusiveSchedulerPair 不是这种情况的正确选择,还是我使用不正确?也许应该在这里使用 AsyncLock (为什么它不是框架的一部分)?或者一个普通的旧 Semaphore 就足够了吗(我知道那时我不会得到 reader-writer 部门,但也许没关系)?

async 方法[1] 的一种思考方式是在每个 await 点将它们拆分为任务。要使用您的示例:

private async Task WriteToFile()
{
  using (var fileStream = new FileStream("file.txt", FileMode.OpenOrCreate, FileAccess.Write))
  using (var writer = new StreamWriter(fileStream))
  {
    await Task.Delay(500); // some other work
    await writer.WriteLineAsync("Lorem ipsum");
  }
}

概念上分为三个任务:

  1. 创建 fileStreamwriter 并启动 Task.Delay(一些其他工作)的任务。
  2. 观察Task.Delay结果并启动WriteLineAsync的任务。
  3. 观察WriteLineAsync的结果并处理writerfileStream的任务。

ConcurrentExclusiveSchedulerPair是一个任务调度器,所以它的语义只在有任务运行ning代码时才适用。当 WriteToFile 是 运行 和 ExclusiveScheduler 时,它持有独占调度程序锁,而任务 (1) 是 运行ning。一旦 Task.Delay 代码开始,任务 (1) 就完成了,它释放了独占调度程序锁。在 500 毫秒延迟期间,独占调度程序锁 持有。一旦延迟完成,任务 (2) 就准备就绪并排队到 ExclusiveScheduler。然后它获取独占调度程序锁并完成它的(少量)工作。当任务 (2) 完成时,它还会释放独占调度程序锁。等等

任务计划程序旨在处理同步任务。在 await 中对它们有一些支持(即,TaskScheduler.Current 被自动捕获并用于从 await 恢复),但通常它们在处理异步任务时没有预期的语义.每个 await 实际上是在告诉任务调度器 "this (sub)task is done".

Does this mean the ConcurrentExclusiveSchedulerPair is not the right choice for this situation?

是的。 ConcurrentExclusiveSchedulerPair 不是正确的选择。

Perhaps an AsyncLock should be used here instead (why isn't it a part of the framework)?

SemaphoreSlim 支持异步锁定(使用更笨拙的语法)。但它可能不在 WP81 上——我不记得了(WaitAsync 是后来添加的)。要支持这么旧的平台,您必须使用 AsyncEx v4 或 copy/paste Stephen Toub's code.

I understand I wouldn't get the reader-writer division then, but maybe it's ok?

这不仅没问题,而且几乎可以肯定是更可取的。当你真的只需要一个轻量级锁时使用 reader/writer 锁是一个非常常见的错误。特别是,仅仅因为某些代码具有读取语义而其他代码具有写入语义不是使用 RWL 的充分理由。

[1] 出于效率原因,async 方法在 await 点被分解为代码块,但这些单独的代码块实际上并未被 Task 对象包装除非 TaskScheduler 存在。