【问题标题】:Resource locking between iterations of the main thread (Async/Await)主线程迭代之间的资源锁定(Async/Await)
【发布时间】:2013-04-24 13:47:16
【问题描述】:

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

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

button2被点击时,它只是直接调用_IndependentResourceModifierAsync()。它本身不会锁定。

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

我要保证两件事:

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

【问题讨论】:

  • “第 1 件事”永远不会发生。
  • @HenkHolterman:为什么不呢?由于使用了异步,资源可以在不阻塞 UI 线程的情况下被“拥有”。
  • 是的,我错过了等待。但是在锁内使用它似乎很奇怪。
  • 我在锁中使用await Task 的原因是因为我需要GUI 立即知道它是否可以执行操作。如果不能,我可能会显示一条错误消息,并要求他们在最后一个操作完成后尝试。
  • 你真的需要嵌套锁定吗?我相信这通常被认为是一种不好的做法。为什么不解除锁定 _IndependentResourceModiferAsync() 并让被调用者处理呢?

标签: c# locking async-await


【解决方案1】:

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

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

当 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 开始(仅靠它本身很难理解),然后使用自定义等待对象在最终用户async 方法中的正确时间“注入”代码。

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

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

【讨论】:

  • 非常感谢您深入回答为什么我不应该这样做,我应该做什么以及无论如何我该怎么做(尤其是这个)。我会像你建议的那样去做。我真的很喜欢你用IDisposable 锁和using 做的事情。这是我第一次看到这种模式。
  • 关于AnonymousDisposable的快速问题。我了解了它的工作原理以及如何轻松编写它,但我假设这不是 .NET 框架的一部分?我通过谷歌看到的唯一“文档”是 StackOverflow 答案。干杯。
  • 对,AnonymousDisposable 不是提供的类型。不过,它真的很容易写,我在回答中称它为AnonymousDisposable,因为这是它的通用名称。如果你想要最好的性能,你应该有一个真正的一次性类型。
【解决方案2】:

我相信表现得相当好的异步重入锁定是不可能的。这是因为当你启动一个异步操作时,你不需要立即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 有一些好处,但对于任何异步兼容锁来说,同样如此。你也不必await TaskSemaphoreSlim.WaitAsync 返回。
  • @StephenCleary 我的意思是,如果您采用不可重入锁,然后与其他一些代码并行执行“不安全”(没有任何锁定)方法,那么代码是您的错现在马车。但是,如果您采用可重入锁,然后与其他东西并行执行“安全”(带有自己的锁定)方法,可能会有一些期望这样做,但实际上它同样有问题。
【解决方案3】:

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

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

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

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

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

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

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

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

【讨论】:

  • 谢谢乔恩。我想过一个信号量,但是当有嵌套锁时它不起作用。
  • “它不起作用”是什么意思?听起来基本上你应该避免嵌套锁开始......
  • 避免嵌套锁可以让我使用信号量。但是,我喜欢尽可能保持锁的独立性,这样调用者就不必担心了。我认为这是合理的。我对“不可能”的答案很满意。
  • @FrankWeindel:但是从外观上看,现在锁不是独立的。为了简单起见,我会尽量避免使用嵌套锁......嵌套锁和异步本身就足够复杂,更不用说组合了。但我刚刚注意到一个可能有点解释问题的混淆点 - 现在编辑我的答案。
  • 它在_IndependentResourceModiferAsync() 中自成一体。所以任何调用它的人都不必担心任何锁。 button1_Click() 锁定它的原因也是因为它在调用 _IndependentResourceModiferAsync() 之前/之后对资源做了一些事情。它想保持原子性。但我同意,这两个概念加在一起确实使事情变得过于复杂,我最好只强迫最终调用者抓住锁。
猜你喜欢
  • 2012-09-23
  • 2023-01-11
  • 2012-05-04
  • 1970-01-01
  • 2019-01-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多