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

Popular posts from this blog

Introduction, and welcome!

On Serilog 'log enrichment' feature