【问题标题】:ReaderWriterLockSlim questionsReaderWriterLockSlim 问题
【发布时间】:2018-11-16 22:37:35
【问题描述】:

在他的回答中,https://stackoverflow.com/a/19664437/4919475

斯蒂芬·克利里提到

ReaderWriterLockSlim 是线程仿射锁类型,所以它通常 不能与 async 和 await 一起使用。

他所说的“通常”是什么意思?什么时候可以使用ReaderWriterLockSlim

另外,我在这里看到http://joeduffyblog.com/2007/02/07/introducing-the-new-readerwriterlockslim-in-orcas/ ReaderWriterLockSlim 有不同的怪癖,但这篇文章是从 2007 年开始的。从那以后有变化吗?

【问题讨论】:

  • 嗯,跨 await 持有 RWL 锁是一种非常糟糕的做法。实际上几乎没有人这样做,如果你不小心弄错了,运行时异常会警告你。

标签: c# asynchronous locking


【解决方案1】:

我猜你已经发布了一个只有 Cleary 才能回答的问题,因为你想知道是什么意思。

与此同时,从他的声明中可以明显推断出,在任何情况下,只要你能够保证获得锁的同一个线程也可以使用 ReaderWriterLockSlimasync/await,你就可以逃脱惩罚。能够释放它。

例如,你可以想象这样的代码:

private readonly ReaderWriterLockSlim _rwls = new ReaderWriterLockSlim();

async void button1_Click(object sender, EventArgs e)
{
    _rwls.EnterWriteLock();
    await ...;
    _rwls.ExitWriteLock();
}

在上面,因为Click事件将在await将返回的线程中引发,你可以获取锁,执行await,仍然可以在继续释放锁,因为你知道它会是同一个线程。

async/await 的许多其他用法中,不能保证继续在方法产生的线程中,因此不允许释放在之前获得的锁await。在某些情况下,这是明确有意的(即ConfigureAwait(false)),在其他情况下,这只是await 上下文的自然结果。无论哪种方式,这些场景都与ReaderWriterLockSlim 不兼容,就像Click 示例一样。

(我故意忽略了一个更大的问题,即获取锁然后在可能长时间运行的异步操作期间持有它是否是个好主意。也就是说,正如他们所说, “一个完整的'另一个'蜡球”。)

附录:

关于“更大的问题”我忽略了一个“短”评论,它太长而不能成为真正的评论……

“更大的问题”相当广泛且高度依赖于上下文。这就是为什么我没有解决它。短版分为两部分:

  1. 一般来说,锁应该持有很短的时间,但一般来说异步操作的持续时间可能很长,所以这两者是互不相容的。在进行并发操作时,锁是一种必要的邪恶,但它们总是会在某种程度上否定并发操作的好处,因为它们具有序列化其他并发操作的效果。

    持有锁的时间越长,一个或多个线程被阻塞等待某事的可能性越大,序列化他们拥有的任何工作。它们都在同一个锁上等待,所以即使长时间运行的锁被释放,它们仍然必须按顺序工作,而不是同时工作。这有点像堵车,一排排的汽车在等待施工卡车完成堵塞道路……即使卡车离开,也需要很长时间才能清除堵塞。
    我不会说在异步操作期间持有锁固有是不好的——也就是说,我可以想象经过深思熟虑的场景,它会是好的——但它经常会破坏其他实现目标,并且在某些情况下可以完全撤消旨在高度并发的设计,尤其是在没有特别注意的情况下。

  2. 在语义上很容易出错,即使用await,您知道锁定会在一段时间内保持不变,但“即发即弃”并不少见,并且会导致代码在正在发生异步操作,但实际上并没有发生(请参阅 Stack Overflow 问题 What happens to a lock during an Invoke/BeginInvoke? (event dispatching) 以获取正是这样做的人的示例,甚至没有意识到这一点)。避免错误代码的一种方法是简单地避免已知可能导致错误的编码模式。

    同样,如果一个人足够小心,就可以避免错误。但通常最好简单地更改实现以使用不那么棘手的方法,并养成这样做的习惯。

【讨论】:

  • 谢谢!如果获取锁然后在异步期间持有它是一个好主意,你能不能故意忽略更大的问题? :) 如果您能详细说明一下,我将不胜感激!再次感谢
  • @Don:请参阅编辑。我认为这就是目前值得一提的内容,没有更多的背景和一些特定的场景。
【解决方案2】:

我在this question 上注意到你问过:

你能解释一下“任意代码”是什么意思吗?

我相信这篇笔记突出了“更大的问题”的一个重要方面,我将尝试——简要地说,因为我也很赶时间——在这里解决。这里的主要问题之一是 await 语句不能保证它等待的 Task 将在与调用代码相同的上下文中运行(特别是在线程仿射锁的情况下,在同一个线程上);事实上,这会破坏Task 承诺的大部分目的。

假设您正在等待的Task,在某处等待使用Task.Run 创建的Task,否则在另一个线程上,或者已经让当前线程等待一些后台资源(如磁盘或网络输入/输出)。在这些情况下,至少有两种很容易意外遇到的意外行为:

  1. 如果在另一个线程中执行的代码试图获得与正在等待它的调用代码相同的锁;调用线程拥有锁,并且由于子任务在不同的线程上执行,它无法获得锁,直到调用线程释放它,它不会这样做,因为它正在等待尚未完成的子任务。如果第二次锁定尝试与第一次锁定在同一线程上,则锁定将识别该线程已经获得锁定并允许第二次锁定尝试继续进行。由于它们不在同一个线程上,这将成为一个自依赖死锁,并且将停止调用线程和子任务或超时,具体取决于所使用的锁定方法。大多数其他死锁需要在多个代码路径中以不同顺序使用 2 个或更多锁,其中每个路径持有另一个正在等待的锁。

  2. 如果调用线程是 UI 线程(或其他带有消息泵的上下文,可以在前一个请求等待异步行为时继续处理请求),假设它等待在另一个线程中执行的 Task需要足够长的时间来处理消息泵开始处理另一条消息(例如再次单击同一个按钮,或任何其他可能需要相同锁的“任意代码”),该新消息正在拥有锁的同一线程上执行因此即使之前的Task 尚未完成也允许继续,从而允许任意访问本应同步的资源。

虽然前者可能会导致您的应用程序或其中的某些组件被锁定,但后者可能会产生非常意想不到的结果,并且特别难以排除故障。所有线程仿射锁定机制都存在类似的情况(例如Monitor,它是lock 关键字的底层实现)。希望对您有所帮助。

如果您对 C# 中的并行模式感兴趣,我可能会推荐免费的 Threading in C# 电子书(这实际上是一本非常优秀的书“C# in a Nutshell”的节选)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-01-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-06-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多