On AsyncLazy in .NET
Today I saw a post on linkedin on combining Lazy<T> with a Task, and then awaiting on lazy.Value, and if developers should ever use such an approach. A few thoughts came to mind about why we should not, for example, inability to pass a cancellation token in a transparent manner and overall obscurity of applying a strictly non-async pattern to an async operation. But then I thought about how would I implement a real async lazy wrapper.
Below is what came out. This is just an idea, though it does look quite usable :)
public sealed class AsyncLazy<T> : IDisposable where T : class {
private const int MaxSpinWaitCount = 10;
private enum Stages {
Uninitialized = 0,
Producing,
Produced
}
private ManualResetEventSlim? resetEvent = new (false, MaxSpinWaitCount);
private readonly Func<CancellationToken, Task<T>> producer;
private T value;
private int stage = (int)Stages.Uninitialized;
public AsyncLazy(Func<CancellationToken, Task<T>> producer) {
this.producer = producer;
}
public bool IsValueCreated => stage == (int)Stages.Produced;
public async ValueTask<T> GetValue(CancellationToken ct) {
if (IsValueCreated) {
return value;
}
if (Interlocked.CompareExchange(ref stage, (int)Stages.Uninitialized, (int)Stages.Producing) == (int)Stages.Uninitialized) {
Interlocked.Exchange(ref value, await producer(ct));
ct.ThrowIfCancellationRequested();
Interlocked.Exchange(ref stage, (int)Stages.Produced);
resetEvent!.Set();
return value;
}
resetEvent!.Wait(ct);
return value;
}
public void Dispose() {
var holder = Interlocked.Exchange(ref resetEvent, null);
holder?.Dispose();
}
}
This code is fully thread-safe, and should throw an OperationCancelledException if the value generation was cancelled via its cancellation token source.
Comments
Post a Comment