from Stephen Cleary's
NuGet package (v3.0.1) to protect the initialization of an expensive resource, so only the first caller will perform the time-consuming asynchronous initialization, and all subsequent callers will wait asynchronously until the initialization is done and then get the cached resource.
What I first noticed is that the code following the region protected by the
was executing in the tasks in the exact
reverse order the tasks were started in (i.e. the last task started got to continue past the locked region first, then the second to last task, and so on until the first task continued last).
Then in the process of investigating why that was happening I discovered I consistently get a stack overflow when there are a large number of the asynchronous tasks. Here's a simplified example:
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.
_foo = new object();
async Task DoStuffAsync()
object foo = await GetFooAsync().ConfigureAwait(false);
// Do stuff with foo...
var tasks = new List<Task>();
for (int i = 1; i <= 1000; i++)
If the fast path through
is not synchronous (e.g. if I add
) then not only does the stack overflow not occur, but the tasks continue past the locked region in the order they were started in.
I'll probably be changing my code to use
from AsyncEx instead for this use case, which I've tested and does not seem to exhibit this problem.
However, I'd like to know if this problem is due to a mistake in my code, a bug in
, or is it just expected behavior (more of a gotcha)?