.NET Fiddle

CSharp · Console

/Program.cs

using System;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;

public class Program
{
    public static async Task Main()
    {
        int executionsCounter = 0;
        int concurrencyCounter = 0;

        AsyncLazy<int> asyncExpiringLazy = new(async () =>
        {
            var executions = Interlocked.Increment(ref executionsCounter);
            var concurrency = Interlocked.Increment(ref concurrencyCounter);
            try
            {
                Print($"**Factory invoked ({executions}), Concurrency: {concurrency}");
                Thread.Sleep(300); // Simulate a blocking operation.
                await Task.Delay(200); // Simulate an I/O-bound operation.
                if (executions <= 2) throw new ApplicationException($"Oops! ({executions})");
                return executions;
            }
            finally { Interlocked.Decrement(ref concurrencyCounter); }
        }, retryOnFailure: true);

        Task[] workers = Enumerable.Range(1, 12).Select(n => Task.Run(async () =>
        {
            await Task.Delay((n - 1) * 200);
            Print($"Worker #{n} requesting value");
            var stopwatch = Stopwatch.StartNew();
            // Measure the duration of getting the task.
            var task = asyncExpiringLazy.Task;
            var duration1 = stopwatch.ElapsedMilliseconds; stopwatch.Restart();
            // Measure the duration of awaiting the task.
            try { await task; } catch { }
            var duration2 = stopwatch.ElapsedMilliseconds;
            string timeInfo = $" (blocked for {duration1:#,0} msec, awaited for {duration2:#,0} msec)";
            try
            {
                var result = await task;
                Print($"--Worker #{n} received value: {result}{timeInfo}");
            }
            catch (Exception ex)
            {
                Print($"--Worker #{n} failed: {ex.Message}{timeInfo}");
            }
        })).ToArray();
        await Task.WhenAll(workers);
        Print($"Finished");
    }

    /// <summary>
    /// Represents the result of an asynchronous operation that is invoked lazily
    /// on demand, with the option to retry it as many times as needed until it
    /// succeeds, while enforcing a non-overlapping execution policy.
    /// </summary>
    public class AsyncLazy<TResult>
    {
        private Func<Task<TResult>> _taskFactory;
        private readonly bool _retryOnFailure;
        private Task<TResult> _task;

        public AsyncLazy(Func<Task<TResult>> taskFactory, bool retryOnFailure = false)
        {
            ArgumentNullException.ThrowIfNull(taskFactory);
            _taskFactory = taskFactory;
            _retryOnFailure = retryOnFailure;
        }

        public Task<TResult> Task
        {
            get
            {
                var capturedTask = Volatile.Read(ref _task);
                if (capturedTask is not null) return capturedTask;

                var newTaskTask = new Task<Task<TResult>>(_taskFactory);
                Task<TResult> newTask = null;
                newTask = newTaskTask.Unwrap().ContinueWith(task =>
                {
                    if (task.IsCompletedSuccessfully || !_retryOnFailure)
                    {
                        _taskFactory = null; // No longer needed (let it get recycled)
                        return task;
                    }
                    // Discard the stored _task, to trigger a retry later.
                    var original = Interlocked.Exchange(ref _task, null);
                    Debug.Assert(ReferenceEquals(original, newTask));
                    return task;
                }, default, TaskContinuationOptions.DenyChildAttach |
                    TaskContinuationOptions.ExecuteSynchronously,
                    TaskScheduler.Default).Unwrap();
                capturedTask = Interlocked
                    .CompareExchange(ref _task, newTask, null) ?? newTask;
                if (ReferenceEquals(capturedTask, newTask))
                    newTaskTask.RunSynchronously(TaskScheduler.Default);
                return capturedTask;
            }
        }

        public TaskAwaiter<TResult> GetAwaiter() => Task.GetAwaiter();

        public ConfiguredTaskAwaitable<TResult> ConfigureAwait(
            bool continueOnCapturedContext)
            => Task.ConfigureAwait(continueOnCapturedContext);
    }

    private static void Print(object value)
    {
        Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} {($"[{Thread.CurrentThread.ManagedThreadId}]"),4} > {value}");
    }
}
;