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

我首先注意到的是,在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();
}


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

在本用例中,我可能会更改代码以使用AsyncEx中的AsyncLazy<T>进行替代,该用例已经过测试,似乎没有出现此问题。

但是,我想知道此问题是由于我的代码错误,AsyncLock中的错误还是仅是预期的行为(更多的原因)?

最佳答案

这是bug in AsyncLock;所有基于队列的异步协调原语都有相同的问题。正在进行修复。

new version of this library具有一个重写的队列,不会受到此问题的困扰。

关于c# - Nito.AsyncEx.AsyncLock堆栈溢出,包含大量等待者和同步快速路径,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/39779088/

10-13 03:40