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

On Serilog 'log enrichment' feature

On 'leaky abstractions'

On developer interview questions