using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
public static async Task Main()
int executionsCounter = 0;
int concurrencyCounter = 0;
AsyncExpiringLazy<int> asyncExpiringLazy = new(async () =>
var executions = Interlocked.Increment(ref executionsCounter);
var concurrency = Interlocked.Increment(ref concurrencyCounter);
Print($"**Factory invoked ({executions}), Concurrency: {concurrency}");
if (executions == 2) throw new ApplicationException($"Oops! ({executions})");
finally { Interlocked.Decrement(ref concurrencyCounter); }
}, _ => TimeSpan.FromMilliseconds(1000));
Task[] workers = Enumerable.Range(1, 24).Select(n => Task.Run(async () =>
await Task.Delay((n - 1) * 200);
Print($"Worker #{n} requesting value");
var stopwatch = Stopwatch.StartNew();
var task = asyncExpiringLazy.Task;
var duration1 = stopwatch.ElapsedMilliseconds; stopwatch.Restart();
try { await task; } catch { }
var duration2 = stopwatch.ElapsedMilliseconds;
string timeInfo = $" (blocked for {duration1:#,0} msec, awaited for {duration2:#,0} msec)";
Print($"--Worker #{n} received value: {result}{timeInfo}");
Print($"--Worker #{n} failed: {ex.Message}{timeInfo}");
await Task.WhenAll(workers);
public class AsyncExpiringLazy<TResult>
private readonly object _locker = new object();
private readonly Func<Task<TResult>> _taskFactory;
private readonly Func<TResult, TimeSpan> _expirationSelector;
private record struct State(Task<TResult> Task, long ExpirationTimestamp);
public AsyncExpiringLazy(Func<Task<TResult>> taskFactory,
Func<TResult, TimeSpan> expirationSelector)
ArgumentNullException.ThrowIfNull(taskFactory);
ArgumentNullException.ThrowIfNull(expirationSelector);
_taskFactory = taskFactory;
_expirationSelector = expirationSelector;
public AsyncExpiringLazy(Func<TResult> valueFactory,
Func<TResult, TimeSpan> expirationSelector)
ArgumentNullException.ThrowIfNull(valueFactory);
ArgumentNullException.ThrowIfNull(expirationSelector);
_taskFactory = () => System.Threading.Tasks.Task.FromResult(valueFactory());
_expirationSelector = expirationSelector;
private Task<TResult> GetTask()
Task<Task<TResult>> newTaskTask;
if (_state.Task is not null
&& _state.ExpirationTimestamp > Environment.TickCount64)
newTaskTask = new(_taskFactory);
newTask = newTaskTask.Unwrap().ContinueWith(task =>
State newState = default;
if (task.IsCompletedSuccessfully)
TimeSpan expiration = _expirationSelector(task.Result);
if (expiration > TimeSpan.Zero)
newState = new State(task, Environment.TickCount64
+ (long)expiration.TotalMilliseconds);
lock (_locker) _state = newState;
}, default, TaskContinuationOptions.DenyChildAttach |
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default).Unwrap();
_state = new State(newTask, Int64.MaxValue);
newTaskTask.RunSynchronously(TaskScheduler.Default);
public Task<TResult> Task => GetTask();
public TResult Result => GetTask().GetAwaiter().GetResult();
public TaskAwaiter<TResult> GetAwaiter() => GetTask().GetAwaiter();
public ConfiguredTaskAwaitable<TResult> ConfigureAwait(
bool continueOnCapturedContext)
=> GetTask().ConfigureAwait(continueOnCapturedContext);
public bool ExpireImmediately()
if (_state.Task is null) return false;
if (!_state.Task.IsCompleted) return false;
private static void Print(object value)
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} {($"[{Thread.CurrentThread.ManagedThreadId}]"),4} > {value}");