C#中的锁和异步方法

75

我不是很清楚(并且找不到足够清晰的文档):在使用lock关键字来编写async方法时:如果对象已被锁定,线程会被阻塞还是会返回一个处于暂停状态的任务(不会阻塞线程,并且在锁定释放后返回)?

在下面的代码中,这行代码会阻塞线程吗?如果它会阻塞线程(这是我想的),是否有标准的非阻塞解决方案? 我正在考虑使用AsyncLock,但首先我想尝试一些标准的方法。

private object myLock = new object(); 

private async Task MyMethod1()
{
    lock (myLock) // <---- will this line cause a return of the current method
                  // as an Await method call would do if myLock was already locked? 
    {
        //.... 
    }
}

// other methods that lock on myLock

3
在这里查看一个很好的解释:https://dev59.com/Z2sz5IYBdhLWcg3w3btb - atomaras
这个回答解决了你的问题吗?为什么不能在lock语句的主体中使用'await'操作符? - Cole Tobin
4个回答

110

为了防止死锁(即开发人员自己伤害自己),这已被禁止。我找到的最好的解决方案是使用信号量 - 请参阅此文章以获取详细信息

相关代码片段:

static SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1);

...

await semaphoreSlim.WaitAsync();
try
{
    await Task.Delay(1000);
}
finally
{
    semaphoreSlim.Release();
}

2
将信号量设置为静态的有什么原因吗?假设您只想锁定该类的实例,则如果信号量是实例变量,它应该同样有效。 - Andy
2
在我的情况下,我有多个对象需要向数据库报告它们的进度。当然,我希望它们是异步的,所以在我的情况下,使用静态SemaphoreSlim是正确的方法。 - Mladen Ristic
谢谢你的回答。SemaphoreSlim 的使用解决了我的问题。 - Silvair L. Soares
@Alexander.Furer 哈哈 - 我们使用信号量在单例模式中的唯一原因是因为单例模式不是线程安全的!而且唯一可以在多个线程之间共享信号量的方法就是将其设为静态的。 - undefined
1
@MartinKirk,哈哈哈,你真逗: services.RegisterSingleton(new Semaphore(1,1)).AsNamed("lockName"),然后将其注入到需要同步的任何服务类型中。使用支持命名/索引服务的Autofac。 - undefined
显示剩余3条评论

84
在下面的代码中,这一行代码会阻塞线程吗?
从技术上讲,是的,但它不会像你期望的那样工作。
有两个原因为什么线程关联锁与异步操作不兼容。一是在一般情况下,异步方法可能不会在相同的线程上恢复执行,因此它会尝试释放自己没有拥有的锁,而另一个线程则一直持有该锁。另一个原因是,在保持锁定时进行等待(await)期间,任意代码可能会在保持锁定的情况下执行。
因此,编译器费尽心思禁止在锁块内使用await表达式。但您仍然可以通过直接使用Monitor或其他基元来自我伤害。
如果它阻塞了线程(我认为是这样),是否有标准的非阻塞解决方案?
是的; SemaphoreSlim类型支持WaitAsync。

6
现在我看到了在lock中使用await的问题,那么相反的情况呢:在最终由某些异步方法调用的方法中使用锁呢?我的猜测是这样做是安全的,因为所有被lock操作锁定的操作都在同一个线程上完成。然而,在这种情况下直接使用监视器仍然存在问题 - 但这很烦人,因为开发人员通常无法确定代码是否会以异步/等待方式使用。有什么建议吗? - KFL
9
只要在锁(或监视器)中只有同步代码,那么它就能正常工作。无论是否由异步方法调用都没有关系。 - Stephen Cleary
5
我当时以为问题是在问“在异步方法中使用锁是否安全”。谢天谢地,我意识到它有些不太清楚,并向下滚动查看了这些评论。哈哈哈。 - user11910061
2
@OfirD:有一个主要问题:async方法在await之后可能会在不同的线程上继续执行,导致一个线程释放了它从未获得的锁定。 - Stephen Cleary
1
@OfirD:现在考虑多个异步方法在同一个线程上运行的情况。1:函数A获取锁并执行await。然后函数B在同一线程上运行;如果它获取相同的锁,则会成功而不是阻塞。此外,在更一般的情况下,函数B可能依赖于由线程2运行的某些代码来获取锁-在这种情况下,您最终会出现死锁,因为线程1持有锁,等待线程2释放锁,而线程2则正在等待线程1释放锁。 - Stephen Cleary
显示剩余5条评论

21

不会。

lockMonitor.EnterMonitor.Exit 的语法糖。使用 lock 会在锁被释放之前一直保持方法的执行。但是,它与 await 完全不同,无论从哪个方面来看都不相似。


1
谢谢。顺便说一句,我猜应用于我的问题:“是的,线程会阻塞”就是我想要的答案。 - rufo
1
lock() 会旋转一小段时间,然后阻塞。如果它一直旋转,它将把低效的代码变成低效的耗时和耗电代码。 - Cory Nelson
我在使用“spin”这个词时表达得不太好。我只是想说,与“await”不同,执行不会离开该方法。我会重新表述的。谢谢@CoryNelson。 - Simon Whitehead

2

您的任务不会处于挂起状态。它将等待myLock解锁后才运行lock语句中的代码。无论您使用什么C#异步模型,这都会发生。

换句话说,除非有许多不同的myLock对象实例,否则没有两个线程能够运行lock语句内部的语句。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接