主线程迭代之间的资源锁定(Async/Await)
本文关键字:Async Await 锁定 资源 线程 迭代 之间 | 更新日期: 2023-09-27 18:10:46
假设我有一个包含两个按钮(button1
和button2
(和一个资源对象(r
(的表单。资源有自己的锁定和解锁代码来处理并发性。任何线程都可以修改资源。
当单击button1
时,其处理程序对r
本身进行一些修改,然后异步调用_IndependentResourceModifierAsync()
,后者对派生任务中的r
进行一些修改。_IndependentResourceModifierAsync()
在此之前获取r
的锁。另外,由于处理程序正在扰乱r
本身,它也获取了r
的锁。
当点击button2
时,它只是直接调用_IndependentResourceModifierAsync()
。它本身不上锁。
正如您所知,按钮的处理程序将始终在主线程上执行(派生的Task
除外(。
有两件事我想保证:
- 如果在主线程锁定资源时单击
button1
或button2
,则会引发异常。(无法使用Monitor
或Mutex
,因为它们是线程驱动的( - 从
button1_Click()
到_IndependentResourceModiferAsync()
的锁嵌套不应导致死锁。(不能使用Semaphore
(
基本上,我认为我正在寻找的是一个"基于堆栈的锁",如果这样的东西存在甚至可能的话。因为当异步方法在等待之后继续时,它会恢复堆栈状态。我找了很多其他有这个问题的人,但都没有找到。这可能意味着我过于复杂了,但我很好奇人们对此有什么看法。我可能遗漏了一些明显的东西。非常感谢。
public class Resource
{
public bool TryLock();
public void Lock();
public void Unlock();
...
}
public class MainForm : Form
{
private Resource r;
private async void button1_Click(object sender, EventArgs e)
{
if (!r.TryLock())
throw InvalidOperationException("Resource already acquired");
try
{
//Mess with r here... then call another procedure that messes with r independently.
await _IndependentResourceModiferAsync();
}
finally
{
r.Unlock();
}
}
private async void button2_Click(object sender, EventArgs e)
{
await _IndependentResourceModifierAsync();
}
private async void _IndependentResourceModiferAsync()
{
//This procedure needs to check the lock too because he can be called independently
if (!r.TryLock())
throw InvalidOperationException("Resource already acquired");
try
{
await Task.Factory.StartNew(new Action(() => {
// Mess around with R for a long time.
}));
}
finally
{
r.Unlock();
}
}
}
资源有自己的锁定和解锁代码来处理并发性。任何线程都可以修改资源。
有一面黄旗。我发现,从长远来看,保护资源(而不是让它们保护自己(的设计通常会更好。
单击button1时,它的处理程序会对r本身进行一些修改,然后异步调用_IndependentResourceModifierAsync((,从而对派生任务中的r进行一些修改_IndependentResourceModifierAsync((在执行此操作之前获取r的锁。另外,由于处理程序正在扰乱r本身,它也获取了r的锁。
还有一面红旗。递归锁几乎总是个坏主意。我在博客上解释我的推理。
我还收到了另一个关于设计的警告:
如果在资源被主线程锁定时单击了button1或button2,则会引发异常。(不能使用监视器或Mutex,因为它们是线程驱动的(
这听起来不对。有别的办法吗?随着状态的变化禁用按钮似乎是一种更好的方法。
我强烈建议重构以消除对锁递归的要求。然后可以使用SemaphoreSlim
和WaitAsync
异步获取锁,使用Wait(0)
进行"尝试锁"。
所以你的代码最终会看起来像这样:
class Resource
{
private readonly SemaphoreSlim mutex = new SemaphoreSlim(1);
// Take the lock immediately, throwing an exception if it isn't available.
public IDisposable ImmediateLock()
{
if (!mutex.Wait(0))
throw new InvalidOperationException("Cannot acquire resource");
return new AnonymousDisposable(() => mutex.Release());
}
// Take the lock asynchronously.
public async Task<IDisposable> LockAsync()
{
await mutex.WaitAsync();
return new AnonymousDisposable(() => mutex.Release());
}
}
async void button1Click(..)
{
using (r.ImmediateLock())
{
... // mess with r
await _IndependentResourceModiferUnsafeAsync();
}
}
async void button2Click(..)
{
using (r.ImmediateLock())
{
await _IndependentResourceModiferUnsafeAsync();
}
}
async Task _IndependentResourceModiferAsync()
{
using (await r.LockAsync())
{
await _IndependentResourceModiferUnsafeAsync();
}
}
async Task _IndependentResourceModiferUnsafeAsync()
{
... // code here assumes it owns the resource lock
}
我找了很多其他有这个问题的人,但都没有找到。这可能意味着我过于复杂化了,但我很好奇人们对此有什么看法
在很长一段时间里,这是不可能的(完全,句号,句号(。有了.NET 4.5,这是可能的,但并不美观。这很复杂。我不知道有人在生产中真的这样做,我当然不建议这样做。
也就是说,我一直在用异步递归锁作为我的AsyncEx库中的一个例子(它永远不会是公共API的一部分(。您可以这样使用它(遵循已经取消的令牌同步操作的AsyncEx约定(:
class Resource
{
private readonly RecursiveAsyncLock mutex = new RecursiveAsyncLock();
public RecursiveLockAsync.RecursiveLockAwaitable LockAsync(bool immediate = false)
{
if (immediate)
return mutex.LockAsync(new CancellationToken(true));
return mutex.LockAsync();
}
}
async void button1Click(..)
{
using (r.LockAsync(true))
{
... // mess with r
await _IndependentResourceModiferAsync();
}
}
async void button2Click(..)
{
using (r.LockAsync(true))
{
await _IndependentResourceModiferAsync();
}
}
async Task _IndependentResourceModiferAsync()
{
using (await r.LockAsync())
{
...
}
}
RecursiveAsyncLock
的代码不是很长,但想想就让人非常头疼。它从我在博客上详细描述的隐式异步上下文开始(这本身很难理解(,然后使用自定义awaitables在正确的时间在最终用户async
方法中"注入"代码。
你正处于任何人尝试过的边缘。RecursiveAsyncLock
根本没有经过彻底的测试,而且可能永远不会。
小心踩,探险者。这是龙。
我认为异步重入锁定不可能表现得相当好。这是因为当您启动异步操作时,不需要立即await
。
例如,假设您将事件处理程序更改为以下内容:
private async void button1_Click(object sender, EventArgs e)
{
if (!r.TryLock())
throw InvalidOperationException("Resource already acquired");
try
{
var task = _IndependentResourceModiferAsync();
// Mess with r here
await task;
}
finally
{
r.Unlock();
}
}
如果锁是异步重入的,那么在事件处理程序中使用r
的代码和调用的异步方法中的代码可以同时工作(因为它们可以在不同的线程上运行(。这意味着这样的锁不安全。
我认为你应该看看SemaphoreSlim
(计数为1(:
- 它不是可重入的(它不属于线程(
- 支持异步等待(
WaitAsync
(
我现在没有时间查看你的场景,但我认为这很合适。
编辑:我刚刚注意到这个问题:
因为当异步方法在等待之后继续时,它会恢复堆栈状态。
不,绝对不是。这很容易展示——添加一个异步方法,它可以响应如下按钮点击:
public void HandleClick(object sender, EventArgs e)
{
Console.WriteLine("Before");
await Task.Delay(1000);
Console.WriteLine("After");
}
在您的两个Console.WriteLine
调用上设置一个断点-您会注意到在await
之前,您有一个堆栈跟踪,其中包括WinForms中的"按钮处理"代码;之后,堆栈将看起来非常不同。