2

假设我有一个带有两个按钮 (button1button2) 和一个资源对象 ( r) 的表单。资源有自己的锁定和解锁代码来处理并发。资源可以被任何线程修改。

单击时button1,它的处理程序会对其r自身进行一些修改,然后_IndependentResourceModifierAsync()异步调用它r在生成的任务中进行一些修改。_IndependentResourceModifierAsync()在执行此操作之前获取r' 锁。也因为处理程序r自己搞砸了,它也获得了r' 锁。

单击时button2,它只是_IndependentResourceModifierAsync()直接调用。它本身没有锁定。

如您所知,按钮的处理程序将始终在主线程上执行(除了 spawned Task)。

我想保证两件事:

  1. 如果在主线程锁定资源时单击button1button2单击,将引发异常。(不能使用MonitororMutex因为它们是线程驱动的)
  2. button1_Click()从through嵌套的锁_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();
            }
    }
}
4

3 回答 3

4

我相信表现得相当好的异步重入锁定是不可能的。这是因为当您启动异步操作时,您不需要立即执行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在事件处理程序中使用的代码和在调用的异步方法中的代码可以同时工作(因为它们可以在不同的线程上运行)。这意味着这种锁是不安全的。

于 2013-04-24T14:54:14.483 回答
4

资源有自己的锁定和解锁代码来处理并发。资源可以被任何线程修改。

有一面黄旗。我发现从长远来看,保护资源(而不是让它们保护自己)的设计通常会更好。

当 button1 被点击时,它的处理程序会对 r 本身进行一些修改,然后异步调用 _IndependentResourceModifierAsync() ,这会在生成的任务中对 r 进行一些修改。_IndependentResourceModifierAsync() 在执行此操作之前获取 r 的锁。也因为处理程序正在弄乱 r 本身,它也获得了 r 的锁。

还有一面红旗。递归锁几乎总是一个坏主意。我在博客上解释了我的推理。

我还收到了另一个关于设计的警告:

如果在主线程锁定资源时单击 button1 或 button2,则会引发异常。(不能使用 Monitor 或 Mutex,因为它们是线程驱动的)

这对我来说听起来不对。有没有其他方法可以做到这一点?随着状态的变化禁用按钮似乎是一种更好的方法。


我强烈建议重构以消除对锁递归的要求。然后您可以使用SemaphoreSlimwithWaitAsync异步获取锁和Wait(0)“try-lock”。

所以你的代码最终看起来像这样:

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不是很长,但想想就非常令人费解。它从我在博客中详细描述的隐式异步上下文开始(仅靠它本身很难理解),然后使用自定义等待对象在最终用户async方法中的正确时间“注入”代码。

你正处于任何人试验过的边缘。RecursiveAsyncLock根本没有经过彻底测试,而且很可能永远不会。

小心行事,探险家。这里是龙。

于 2013-04-25T00:18:30.070 回答
3

我认为你应该看看SemaphoreSlim(计数为 1):

  • 它不是可重入的(它不属于线程)
  • 支持异步等待(WaitAsync

我现在没有时间检查您的方案,但我认为它适合。

编辑:我刚刚注意到这个问题:

因为当异步方法在等待之后继续时,它会恢复堆栈状态。

不,绝对没有。这很容易显示 - 添加一个响应按钮单击的异步方法,如下所示:

public void HandleClick(object sender, EventArgs e)
{
    Console.WriteLine("Before");
    await Task.Delay(1000);
    Console.WriteLine("After");
}

在你的两个调用上设置一个断点Console.WriteLine——你会注意到在之前await你有一个堆栈跟踪,包括 WinForms 中的“按钮处理”代码;之后堆栈看起来会非常不同。

于 2013-04-24T13:51:15.563 回答