主线程迭代之间的资源锁定(Async / Await)

时间:2013-04-24 13:47:17

标签: c# locking async-await

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

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

点击button2后,它只会直接调用_IndependentResourceModifierAsync()。它本身没有锁定。

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

我想保证两件事:

  1. 如果在主线程锁定资源时单击button1button2,则会引发异常。 (不能使用MonitorMutex,因为它们是线程驱动的)
  2. button1_Click()_IndependentResourceModiferAsync()的锁定嵌套不应导致死锁。 (不能使用Semaphore)。
  3. 基本上,我认为我正在寻找的是“基于堆栈的锁定”,如果这样的事情存在或者甚至是可能的话。因为在await之后异步方法继续时,它会恢复堆栈状态。我做了很多寻找其他遇到这个问题的人但是干了。这可能意味着我的事情过于复杂,但我很好奇人们对它的看法。可能有一些我很遗憾的事情。非常感谢。

    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();
                }
        }
    }
    

3 个答案:

答案 0 :(得分: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的代码和调用的异步方法中的代码可以同时工作(因为它们可以在不同的线程上运行)。这意味着这种锁定不安全。

答案 1 :(得分:4)

  

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

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

  

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

还有一面红旗。递归锁几乎总是一个坏主意。 I explain my reasoning on my blog.

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

  

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

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


我强烈建议重构以删除锁递归的要求。然后,您可以使用SemaphoreSlimWaitAsync异步获取锁定,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库中使用asynchronous recursive locks as an example(它永远不会成为公共API的一部分)。您可以像这样使用它(在AsyncEx convention of already-cancelled tokens acting synchronously之后):

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的代码不是很长,但考虑到它是非常令人费解的。它从我在我的博客上详细描述的implicit async context开始(这本身很难理解),然后使用自定义等待在最终用户{{1}的恰当时间“注入”代码方法。

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

小心谨慎,探险家。这是龙。

答案 2 :(得分:3)

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

  • 它不是可重入的(它不是由线程拥有)
  • 它支持异步等待(WaitAsync

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

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

  

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

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

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

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