Nito.AsyncEx.AsyncLock 堆栈溢出,大量等待者和同步快速路径
Nito.AsyncEx.AsyncLock stack overflow with lots of awaiters and synchronous fast path
我正在使用 Stephen Cleary 的 Nito.AsyncEx
NuGet 包 (v3.0.1) 中的 AsyncLock
来保护昂贵资源的初始化,因此只有第一个调用者会执行耗时的异步初始化,所有后续调用者将异步等待,直到初始化完成,然后获取缓存资源。
我首先注意到的是,受 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()
的快速路径不是同步的(例如,如果我在 return _foo;
之前添加 await Task.Yield();
),那么不仅不会发生堆栈溢出,而且任务会继续按照开始的顺序通过锁定区域。
我可能会更改我的代码以使用来自 AsyncEx 的 AsyncLazy<T>
而不是这个用例,我已经测试过并且似乎没有出现这个问题。
但是,我想知道这个问题是由于我的代码错误、AsyncLock
中的错误还是只是预期的行为(更多的陷阱)?
这是一个bug in AsyncLock
;所有基于队列的异步协调原语都有同样的问题。正在修复中。
new version of this library 有一个重写的队列,不会遇到这个问题。
我正在使用 Stephen Cleary 的 Nito.AsyncEx
NuGet 包 (v3.0.1) 中的 AsyncLock
来保护昂贵资源的初始化,因此只有第一个调用者会执行耗时的异步初始化,所有后续调用者将异步等待,直到初始化完成,然后获取缓存资源。
我首先注意到的是,受 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()
的快速路径不是同步的(例如,如果我在 return _foo;
之前添加 await Task.Yield();
),那么不仅不会发生堆栈溢出,而且任务会继续按照开始的顺序通过锁定区域。
我可能会更改我的代码以使用来自 AsyncEx 的 AsyncLazy<T>
而不是这个用例,我已经测试过并且似乎没有出现这个问题。
但是,我想知道这个问题是由于我的代码错误、AsyncLock
中的错误还是只是预期的行为(更多的陷阱)?
这是一个bug in AsyncLock
;所有基于队列的异步协调原语都有同样的问题。正在修复中。
new version of this library 有一个重写的队列,不会遇到这个问题。