Nito.AsyncEx.AsyncLock 堆栈溢出,有很多等待程序和同步快速路径

Sea*_*ose 2 c# asynchronous async-await .net-4.5

我正在使用AsyncLockStephen Cleary 的Nito.AsyncExNuGet 包 (v3.0.1) 来保护昂贵资源的初始化,因此只有第一个调用者将执行耗时的异步初始化,并且所有后续调用者将异步等待直到初始化完成并且然后获取缓存的资源。

我首先注意到的是,受保护区域后面的代码AsyncLock在任务中以与任务启动的完全相反的顺序执行(即,最后一个任务开始必须首先继续通过锁定区域,然后是倒数第二个任务,依此类推,直到第一个任务最后继续)。

然后在调查为什么会发生这种情况的过程中,我发现当有大量异步任务时,我总是会出现堆栈溢出。这是一个简化的示例:

object _foo;
readonly Nito.AsyncEx.AsyncLock _fooLock = new Nito.AsyncEx.AsyncLock();

async Task<object> GetFooAsync()
{
    using (await _fooLock.LockAsync().ConfigureAwait(false))
    {
        if (_foo == null)
        {
            // Simulate time-consuming asynchronous initialization,
            // during which all the subsequent tasks end up awaiting the AsyncLock.
            await Task.Delay(5000).ConfigureAwait(false);
            _foo = new object();
        }
        return _foo;
    }
}

async Task DoStuffAsync()
{
    object foo = await GetFooAsync().ConfigureAwait(false);
    // Do stuff with foo...
}

void DoStuff()
{
    var tasks = new List<Task>();

    for (int i = 1; i <= 1000; i++)
    {
        tasks.Add(DoStuffAsync());
    }

    Task.WhenAll(tasks).Wait();
}
Run Code Online (Sandbox Code Playgroud)

如果快速路径GetFooAsync()不是同步的(例如,如果我在await Task.Yield();之前添加return _foo;),那么不仅不会发生堆栈溢出,而且任务会按照它们开始的顺序继续经过锁定区域。

AsyncLazy<T>对于这个用例,我可能会更改我的代码以从 AsyncEx 使用,我已经测试过它似乎没有出现这个问题。

但是,我想知道这个问题是由于我的代码中的错误AsyncLock、 .

Ste*_*ary 5

这是一个错误AsyncLock;所有基于队列的异步协调原语都有同样的问题。正在修复中。

这个库新版本有一个重写的队列,不会遇到这个问题。