Stephen Toub的AsyncLazy<T>
实现基于Lazy<Task<T>>
,非常好且简洁,但是有一些问题并不完全符合我的喜好:
如果异步操作失败,则会缓存错误,并将其传播到AsyncLazy<T>
实例的所有未来等待者。没有办法取消已缓存的Task
,以便可以重试异步操作。例如,这使得AsyncLazy<T>
在实现缓存系统时几乎无法使用。
异步委托在ThreadPool
上调用。没有办法在调用线程上调用它。
如果我们尝试通过直接调用taskFactory
委托而不是将其包装在Task.Factory.StartNew
中来解决前一个问题,则在不幸的情况下,该委托阻塞调用线程相当长的时间,所有等待AsyncLazy<T>
实例的线程都将被阻塞,直到委托完成。这是Lazy<T>
类型如何工作的直接结果。该类型从未设计为以任何方式支持异步操作。
Lazy<Task<T>>
组合在Visual Studio 2019(16.8.2)的最新版本中会生成警告。似乎这种组合可能会在某些情况下导致死锁。
Stephen Cleary的AsyncLazy<T>
实现(AsyncEx库的一部分)解决了第一个问题,其构造函数接受一个RetryOnFailure
标志。同样的实现也解决了第二个问题(ExecuteOnCallingThread
标志)。据我所知,第三和第四个问题尚未得到解决。
以下是尝试解决所有这些问题的一种方法。这个实现不是基于
Lazy<Task<T>>
,而是基于一个临时嵌套任务(
Task<Task<T>>
)。
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;
return task;
}
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);
}
使用示例:
var lazyOperation = new AsyncLazy<string>(async () =>
{
return await _httpClient.GetStringAsync("https://stackoverflow.com");
}, retryOnFailure: true);
string html = await lazyOperation;
taskFactory
委托在调用线程上被调用(即在上面的示例中调用
await lazyOperation
的线程)。如果您希望在
ThreadPool
上调用它,您可以更改实现,用
Start
方法替换
RunSynchronously
,或将
taskFactory
包装在
Task.Run
中(例如上面的示例中的
new AsyncLazy<string>(() => Task.Run(async () =>
)。通常情况下,异步委托期望快速返回,因此在调用线程上调用它不应该是一个问题。作为一个奖励,它打开了从委托内部与线程相关的组件(如UI控件)交互的可能性。
这个实现传播所有可能由
taskFactory
委托引发的异常,而不仅仅是第一个异常。在一些情况下,这可能很重要,比如当委托直接返回一个
Task.WhenAll
任务时。要实现这个,首先将
AsyncLazy<T>.Task
存储在一个变量中,然后在
catch
块中检查变量的
Exception.InnerExceptions
属性。
可以在
这里找到
AsyncLazy<T>
类的在线演示。它演示了多个并发工作者使用该类时的行为,以及
taskFactory
失败的情况。
Lazy
,其初始化方法执行你想要的操作。这样可以确保初始化方法只被调用一次。 - Panagiotis KanavosRelease
。 - Daniel KelleyLazy
。 - wilmol