假设我有一个类需要使用 InitializeAsync() 方法执行一些异步初始化。 我想确保初始化只执行一次。如果另一个线程在初始化已经进行时调用此方法,它将“等待”直到第一个调用返回。
我正在考虑以下实现(使用 SemaphoreSlim)。 有更好/更简单的方法吗?
public class MyService : IMyService
{
private readonly SemaphoreSlim mSemaphore = new SemaphoreSlim(1, 1);
private bool mIsInitialized;
public async Task InitializeAsync()
{
if (!mIsInitialized)
{
await mSemaphore.WaitAsync();
if (!mIsInitialized)
{
await DoStuffOnlyOnceAsync();
mIsInitialized = true;
}
mSemaphore.Release();
}
}
private Task DoStuffOnlyOnceAsync()
{
return Task.Run(() =>
{
Thread.Sleep(10000);
});
}
}
谢谢!
编辑:
由于我正在使用 DI 并且该服务将被注入,因此将其作为“惰性”资源使用或使用异步工厂对我来说不起作用(尽管它在其他用例中可能很棒)。 因此,异步初始化应该封装在类中,并对
IMyService
消费者透明。
将初始化代码包装在“虚拟”
AsyncLazy<>
对象中的想法可以完成这项工作,尽管这对我来说有点不自然。
AsyncLazy<T>
(略有修改的版本):
public class AsyncLazy<T> : Lazy<Task<T>>
{
public AsyncLazy(Func<T> valueFactory) :
base(() => Task.Run(valueFactory)) { }
public AsyncLazy(Func<Task<T>> taskFactory) :
base(() => Task.Run(() => taskFactory())) { }
public TaskAwaiter<T> GetAwaiter() { return Value.GetAwaiter(); }
}
然后像这样食用:
private AsyncLazy<bool> asyncLazy = new AsyncLazy<bool>(async () =>
{
await DoStuffOnlyOnceAsync()
return true;
});
注意,我使用
bool
只是因为您没有 DoStuffOnlyOnceAsync
的返回类型。
编辑:
Stephan Cleary(当然)也有这个的实现here。
是的。使用 Stephen Cleary 的
AsyncLazy
(在 AsyncEx
nuget 上可用):
private static readonly AsyncLazy<MyResource> myResource = new AsyncLazy<MyResource>(
async () =>
{
var ret = new MyResource();
await ret.InitAsync();
return ret;
}
);
public async Task UseResource()
{
MyResource resource = await myResource;
// ...
}
或者如果您更喜欢 Microsoft 实现,则可以使用 visual studio SDK 的
AsyncLazy
。
我有一篇博客文章,其中介绍了执行“异步构造函数”的几种不同选项。
通常,我更喜欢异步工厂方法,因为我认为它们更简单并且更安全:
public class MyService
{
private MyService() { }
public static async Task<MyService> CreateAsync()
{
var result = new MyService();
result.Value = await ...;
return result;
}
}
AsyncLazy<T>
是定义共享异步资源的完美方式(并且可能是“服务”更好的概念匹配,具体取决于它的使用方式)。异步工厂方法方法的一个优点是无法创建 MyService
的未初始化版本。
AsyncLazy<T>
实现,基于 Lazy<Task<T>>
,非常漂亮和简洁,但有一些事情并不完全符合我的喜好:
如果异步操作失败,错误将被缓存,并将传播到
AsyncLazy<T>
实例的所有未来等待者。没有办法取消缓存已缓存的Task
,以便重试异步操作。例如,这使得 AsyncLazy<T>
实际上无法用于实现缓存系统。
在
ThreadPool
上调用异步委托。无法在调用线程上调用它。
如果我们尝试通过直接调用
taskFactory
委托而不是将其包装在 Task.Factory.StartNew
中来解决前面的问题,那么在不幸的情况下,委托会阻塞调用线程很长一段时间,所有将await
AsyncLazy<T>
实例将被阻塞,直到委托完成。这是 Lazy<T>
类型工作原理的直接结果。这种类型从来都不是为了以任何方式支持异步操作而设计的。
Lazy<Task<T>>
组合在最新版本的 Visual Studio 2019 (16.8.2) 中生成警告。看来这个组合在某些场景下会产生死锁。
第一个问题已由 Stephen Cleary 的
AsyncLazy<T>
implementation(AsyncEx 库的一部分)解决,它在其构造函数中接受 RetryOnFailure
标志。第二个问题也已通过相同的实现得到解决(ExecuteOnCallingThread
标志)。 AFAIK 第三和第四个问题还没有解决。
以下是解决所有这些问题的尝试。此实现不是基于
Lazy<Task<T>>
,而是基于瞬态嵌套任务 (Task<Task<T>>
)。
/// <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);
}
使用示例:
var lazyOperation = new AsyncLazy<string>(async () =>
{
return await _httpClient.GetStringAsync("https://stackoverflow.com");
}, retryOnFailure: true);
//... (the operation has not started yet)
string html = await lazyOperation;
taskFactory
委托在调用线程(上例中调用await lazyOperation
的线程)上调用。如果您更喜欢在 ThreadPool
上调用它,您可以更改实现并将 RunSynchronously
替换为 Start
方法,或者将 taskFactory
包装在 Task.Run
中(new AsyncLazy<string>(() => Task.Run(async () =>
中)上面的例子)。通常,异步委托预计会快速返回,因此在调用线程上调用它应该不成问题。作为奖励,它提供了从委托内部与线程仿射组件(例如 UI 控件)进行交互的可能性。
此实现传播
taskFactory
委托可能引发的所有异常,而不仅仅是第一个。这在某些情况下可能很重要,例如当委托直接返回 Task.WhenAll
任务时。为此,首先将 AsyncLazy<T>.Task
存储在变量中,然后将 await
存储在变量中,最后在 catch
块中检查变量的 Exception.InnerExceptions
属性。
可以在
这里找到
AsyncLazy<T>
课程的在线演示。它演示了该类被多个并发工作线程使用时的行为,并且 taskFactory
失败了。