ReaderWriterLockSlim问题

时间:2017-05-17 23:11:42

标签: c# asynchronous locking

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

Stephen Cleary提到

  

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

他的意思是"通常"?什么时候可以使用ReaderWriterLockSlim

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

2 个答案:

答案 0 :(得分:4)

我猜你已经发布了一个只有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)以获取完全相同的人的示例,但甚至没有意识到)。避免错误代码的一种方法是简单地避免已知可能导致错误的编码模式。
    再次,如果一个人足够小心,就可以避免错误。但通常最好只是简单地改变实现以使用不那么棘手的方法,并养成这样做的习惯。

答案 1 :(得分:1)

this question上,我注意到您曾问过:

  

您能解释一下“任意代码”的含义吗?

我相信此注释着重强调了“更大的问题”的一个重要方面,我将尝试在此简短地回答,因为我也很忙。这里的主要关注点之一是await语句不能保证它所等待的Task将在与调用代码相同的上下文中运行(特别是在线程仿射锁的情况下,在同一线程上)。实际上,这将破坏Task承诺的许多目的。

比方说,您等待的Task在行的某个地方,等待使用Task创建的Task.Run,或者在另一个线程上,或者产生了当前线程以等待某些背景资源(例如磁盘或网络I / O)。在这种情况下,至少有两种意外的行为很容易意外发生:

  1. 如果在另一个线程中执行的代码试图获得与正在等待它的调用代码相同的锁;调用线程拥有该锁,并且由于子任务是在不同的线程上执行的,因此直到调用线程释放它之前,它才能获得该锁,因为该线程正在等待尚未完成的子任务,所以它将不会获得该锁。如果第二次尝试与第一次尝试在同一线程上,则该锁定将识别出该线程已获取该锁定,并允许第二次尝试进行锁定。由于它们不在同一个线程上,因此这将成为自相关的死锁,并且将取决于所使用的锁定方法而同时终止调用线程和子任务,或者将导致超时。其他大多数死锁都需要在多个代码路径中以不同的顺序使用2个或多个锁,其中每个路径都持有另一个正在等待的锁。

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

尽管前者可能导致您的应用程序或其某些组件锁定,但后者可能会产生非常出乎意料的结果,并且在解决问题时特别棘手。所有线程仿射锁定机制都存在类似的条件(例如Monitor的基础实现lock)。希望有帮助。

如果您对C#中的并行模式感兴趣,我可能会推荐免费的Threading in C#电子书(这实际上是本来不错的书《坚果壳中的C#》的摘录)