简体   繁体   English

主线程迭代之间的资源锁定(异步/等待)

[英]Resource locking between iterations of the main thread (Async/Await)

Let's say I have a form with two buttons ( button1 and button2 ) and a resource object ( r ).假设我有一个带有两个按钮( button1button2 )和一个资源 object ( r ) 的表单。 The resource has its own locking and unlocking code to handle concurrency.资源有自己的锁定和解锁代码来处理并发。 The resource could be modified by any thread.资源可以被任何线程修改。

When button1 is clicked, its handler does some modifying of r itself and then calls _IndependentResourceModifierAsync() asynchronously which does some modifying of r in a spawned task.单击button1时,它的处理程序本身r进行一些修改,然后异步调用_IndependentResourceModifierAsync() ,它会在生成的任务中对r进行一些修改。 _IndependentResourceModifierAsync() acquires r 's lock before doing this. _IndependentResourceModifierAsync()在执行此操作之前获取r的锁。 Also because the handler is messing with r itself, it acquires r 's lock too.也因为处理程序正在与r本身混淆,它也获得了r的锁。

When button2 is clicked, it just calls _IndependentResourceModifierAsync() directly.单击button2时,它只是直接调用_IndependentResourceModifierAsync() It does no locking itself.它不会锁定自己。

As you know, the handlers for the buttons will always execute on the main thread (except for the spawned Task ).如您所知,按钮的处理程序将始终在主线程上执行(生成的Task除外)。

There's two things that I want to guarantee:我想保证两件事:

  1. If either button1 or button2 is clicked while the resource is locked by the main thread, an exception will be thrown.如果在资源被主线程锁定时单击button1button2 ,将抛出异常。 (Can't use a Monitor or Mutex because they are thread driven) (不能使用MonitorMutex ,因为它们是线程驱动的)
  2. The nesting of locks from button1_Click() through _IndependentResourceModiferAsync() should not cause a deadlock.button1_Click()_IndependentResourceModiferAsync()的锁嵌套不应导致死锁。 (Can't use a Semaphore ). (不能使用Semaphore )。

Basically, I think what I'm looking for is a "stack-based lock" if such a thing exists or is even possible.基本上,我认为我正在寻找的是一个“基于堆栈的锁”,如果这样的事情存在或什至可能的话。 Because when an async method continues after an await, it restores stack state. I did a lot of searching for anyone else who has had this problem but came up dry.因为当 async 方法在 await 之后继续时,它会恢复堆栈 state。我做了很多搜索,寻找其他遇到过这个问题但没有找到的人。 That likely means I'm over-complicating things, but I am curious what people have to say about it.这可能意味着我把事情复杂化了,但我很好奇人们对此有何看法。 There might be something really obvious I'm missing.可能有一些非常明显的东西我失踪了。 Many thanks.非常感谢。

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

I believe asynchronous re-entrant locking that behaves reasonably well is impossible.我相信表现得相当好的异步重入锁定是不可能的。 This is because when you start an asynchronous operation, you're not required to immediately await it.这是因为当您启动异步操作时,您不需要立即await它。

For example, imagine you changed your event handler to something like this:例如,假设您将事件处理程序更改为如下内容:

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

If the lock was asynchronously re-entrant, the code that works with r in the event handler and the code in the invoked asynchronous method could work at the same time (because they can run on different threads).如果锁是异步可重入的,则在事件处理程序中与r一起工作的代码和被调用的异步方法中的代码可以同时工作(因为它们可以在不同的线程上运行)。 This means such lock wouldn't be safe.这意味着这种锁是不安全的。

The resource has its own locking and unlocking code to handle concurrency.资源有自己的锁定和解锁代码来处理并发。 The resource could be modified by any thread.资源可以被任何线程修改。

There's a yellow flag.有一面黄旗。 I find that a design where you protect resources (rather than have them protect themselves) is usually better in the long run.我发现从长远来看,保护资源(而不是让它们保护自己)的设计通常会更好。

When button1 is clicked, its handler does some modifying of r itself and then calls _IndependentResourceModifierAsync() asynchronously which does some modifying of r in a spawned task.当 button1 被点击时,它的处理程序会对 r 本身进行一些修改,然后异步调用 _IndependentResourceModifierAsync() ,这会在生成的任务中对 r 进行一些修改。 _IndependentResourceModifierAsync() acquires r's lock before doing this. _IndependentResourceModifierAsync() 在执行此操作之前获取 r 的锁。 Also because the handler is messing with r itself, it acquires r's lock too.也因为处理程序正在弄乱 r 本身,它也获得了 r 的锁。

And there's a red flag.还有一面红旗。 Recursive locks are almost always a bad idea.递归锁几乎总是一个坏主意。 I explain my reasoning on my blog. 我在博客上解释了我的推理。

There's also another warning I picked up regarding the design:我还收到了另一个关于设计的警告:

If either button1 or button2 is clicked while the resource is locked by the main thread, an exception will be thrown.如果在主线程锁定资源时单击 button1 或 button2,则会引发异常。 (Can't use a Monitor or Mutex because they are thread driven) (不能使用 Monitor 或 Mutex,因为它们是线程驱动的)

That doesn't sound right to me.这对我来说听起来不对。 Is there any other way to do this?有没有其他方法可以做到这一点? Disabling buttons as the state changes seems like a much nicer approach.随着状态的变化禁用按钮似乎是一种更好的方法。


I strongly recommend refactoring to remove the requirement for lock recursion.我强烈建议重构以消除对锁递归的要求。 Then you can use SemaphoreSlim with WaitAsync to asynchronously acquire the lock and Wait(0) for the "try-lock".然后你可以使用SemaphoreSlimWaitAsync来异步获取锁和Wait(0)用于“try-lock”。

So your code would end up looking something like this:所以你的代码最终看起来像这样:

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
}

I did a lot of searching for anyone else who has had this problem but came up dry.我做了很多寻找其他有这个问题但没有找到的人。 That likely means I'm over-complicating things, but I am curious what people have to say about it.这可能意味着我把事情复杂化了,但我很好奇人们对此有什么看法。

For a long time, it wasn't possible (at all, period, full-stop).很长一段时间,这是不可能的(完全,句号,句号)。 With .NET 4.5, it is possible, but it's not pretty.使用 .NET 4.5,这是可能的,但它并不漂亮。 It's very complicated.这很复杂。 I'm not aware of anyone actually doing this in production, and I certainly don't recommend it.我不知道有人在生产中实际这样做,我当然不推荐它。

That said, I have been playing around with asynchronous recursive locks as an example in my AsyncEx library (it will never be part of the public API).也就是说,我一直在使用异步递归锁作为我的 AsyncEx 库中的示例(它永远不会成为公共 API 的一部分)。 You can use it like this (following the AsyncEx convention of already-cancelled tokens acting synchronously ):您可以像这样使用它(遵循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())
  {
    ...
  }
}

The code for RecursiveAsyncLock is not very long but it is terribly mind-bending to think about. RecursiveAsyncLock的代码不是很长,但想想就非常令人费解。 It starts with the implicit async context that I describe in detail on my blog (which is hard to understand just by itself) and then uses custom awaitables to "inject" code at just the right time in the end-user async methods.它从我在博客中详细描述的隐式异步上下文开始(仅靠它本身很难理解),然后使用自定义等待对象在最终用户async方法中的正确时间“注入”代码。

You're right at the edge of what anyone has experimented with.你正处于任何人试验过的边缘。 RecursiveAsyncLock is not thoroughly tested at all, and likely never will be. RecursiveAsyncLock根本没有经过彻底测试,而且很可能永远不会。

Tread carefully, explorer.小心行事,探险家。 Here be dragons.这里是龙。

I think you should look at SemaphoreSlim (with a count of 1):我认为您应该查看SemaphoreSlim (计数为 1):

  • It isn't re-entrant (it's not owned by a thread)它不是可重入的(它不属于线程)
  • It supports asynchronous waiting ( WaitAsync )它支持异步等待( WaitAsync

I don't have time to check through your scenario right now, but I think it would fit.我现在没有时间检查您的方案,但我认为它适合。

EDIT: I've just noticed this bit of the question:编辑:我刚刚注意到这个问题:

Because when an async method continues after an await, it restores stack state.因为当异步方法在等待之后继续时,它会恢复堆栈状态。

No, it absolutely doesn't.不,绝对没有。 That's easily to show - add an async method which responds to a button click like this:这很容易显示 - 添加一个响应按钮单击的异步方法,如下所示:

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

Set a break point on both of your Console.WriteLine calls - you'll notice that before the await , you've got a stack trace including the "button handling" code in WinForms;在你的两个Console.WriteLine调用上设置一个断点 - 你会注意到await之前,你有一个堆栈跟踪,包括 WinForms 中的“按钮处理”代码; afterwards the stack will look very different.之后堆栈看起来会非常不同。

Keep in mind that many people have told you to refactor away from lock reentrance.请记住,很多人都告诉过你要重构远离锁重入。 There's a lot of wisdom in that.这里面有很多智慧。 BUT if you don't want to do that then check out this NuGet package that I wrote:但是,如果您不想这样做,请查看我写的这个 NuGet 包:

https://www.nuget.org/packages/ReentrantAsyncLock/ https://www.nuget.org/packages/ReentrantAsyncLock/

This is the only correct async equivalent to Monitor.Enter / lock that I know of.这是我所知道的唯一正确的等效于Monitor.Enter / lock的异步。 It is the only thing I've seen that gives you all three of these at the same time:这是我所见过的唯一同时给你所有这三个的东西:

  • Asynchronicity异步性
  • Reentrance重入
  • Mutual exclusion互斥

You'll notice that the second two of those are what Monitor.Enter / lock gives you.您会注意到其中的后两个是Monitor.Enter / lock给您的。 I think it's valid to call ReentrantAsyncLock the async equivalent.我认为将ReentrantAsyncLock称为异步等效项是有效的。

I believe this is how it would look in your code:我相信这就是它在您的代码中的外观:

public class MainForm : Form
{
    readonly ReentrantAsyncLock r = new();
    private async void button1_Click(object sender, EventArgs e)
    {
        await using (await r.LockAsync(CancellationToken.None))
        {
            //Mess with r here... then call another procedure that messes with r independently.
            await _IndependentResourceModiferAsync();
        }
    }

    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
        await using (await r.LockAsync(CancellationToken.None))
        {
            await Task.Factory.StartNew(new Action(() => {
                // Mess around with R for a long time.
            }));
        }
    }
}

Here's a demonstration of it in the general sense (copied from the package documentation):这是一般意义上的演示(从包文档中复制):

var asyncLock = new ReentrantAsyncLock();
var raceCondition = 0;
// You can acquire the lock asynchronously
await using (await asyncLock.LockAsync(CancellationToken.None))
{
    await Task.WhenAll(
        Task.Run(async () =>
        {
            // The lock is reentrant
            await using (await asyncLock.LockAsync(CancellationToken.None))
            {
                // The lock provides mutual exclusion
                raceCondition++;
            }
        }),
        Task.Run(async () =>
        {
            await using (await asyncLock.LockAsync(CancellationToken.None))
            {
                raceCondition++;
            }
        })
    );
}
Assert.Equal(2, raceCondition);

This is certainly not the first attempt at doing this.这当然不是第一次尝试这样做。 But like I said it's the only correct attempt that I've seen so far.但就像我说的那样,这是迄今为止我见过的唯一正确的尝试。 Some other implementations will deadlock trying to re-enter the lock in one of the Task.Run calls.其他一些实现会在尝试在Task.Run调用之一中重新输入锁时发生死锁。 Others will not actually provide mutual exclusion and the raceCondition variable will sometimes equal 1 instead of 2:其他人实际上不会提供互斥, raceCondition变量有时会等于 1 而不是 2:

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM