假设我有一个需要使用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<>
对象中的想法将完成这项工作,尽管对我来说感觉有点不自然。
答案 0 :(得分:9)
我选择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
的返回类型。
修改强>
答案 1 :(得分:7)
是。使用Stephen Cleary's 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's AsyncLazy
。
答案 2 :(得分:5)
我有blog post that covers a few different options for doing "asynchronous constructors"。
通常,我更喜欢异步工厂方法,因为我认为它们更简单,更安全:
public class MyService
{
private MyService() { }
public static async Task<MyService> CreateAsync()
{
var result = new MyService();
result.Value = await ...;
return result;
}
}
AsyncLazy<T>
是定义共享异步资源的一种非常好的方法(并且可能是“服务”的更好的概念匹配,具体取决于它的使用方式)。异步工厂方法方法的一个优点是无法创建MyService
的未初始化版本。
答案 3 :(得分:1)
Stephen Toub 的 AsyncLazy<T>
实现非常简洁,但有几点我不喜欢:
如果异步操作失败,错误将被缓存,并将传播到 AsyncLazy<T>
实例的所有未来等待者。无法取消缓存缓存的 Task
,以便可以重试异步操作。
异步委托在 ThreadPool
上下文中调用。无法在当前上下文中调用它。
当异步委托完成时,如果附加了多个延续,所有延续将在同一个线程上同步调用。因此,如果一个异步工作流的 await asyncLazy
后跟一些冗长/阻塞的代码,则所有其他等待相同 asyncLazy
的异步工作流都会受到影响(延迟)。此问题特别影响 .NET Framework。对于 .NET Core 和 .NET 5 来说,这不是问题,因为在这些平台上,(非完整)异步生成任务的延续是异步运行的。这不是一个高严重性问题,因为它只影响在完成之前await
AsyncLazy<T>
的工作流。
Lazy<Task<T>>
组合在最新版本的 Visual Studio 2019 (16.8.2) 中生成 warnings。在某些情况下,这种组合似乎 can produce deadlocks。
Stephen Cleary 的 AsyncLazy<T>
implementation(AsyncEx 库的一部分)解决了第一个问题,它在其构造函数中接受 RetryOnFailure
标志。第二个问题也已通过相同的实现(ExecuteOnCallingThread
标志)解决,但方式并非最佳。如果异步委托包含阻塞代码,所有同时访问 AsyncLazy<T>.Task
属性的工作流将被阻塞。这是 Lazy<T>
类工作方式的直接结果。 AFAIK 第三和第四个问题尚未解决。
下面尝试解决所有这些问题。此实现不是基于 Lazy<Task<T>>
,而是在内部使用瞬态嵌套任务 (Task<Task<T>>
) 作为包装器。
/// <summary>
/// Represents a single asynchronous operation that is started on first demand.
/// In case of failure the error is not cached, and the operation is restarted
/// (retried) later on demand.
/// </summary>
public class AsyncLazy<TResult>
{
private Func<Task<TResult>> _factory;
private Task<TResult> _task;
public AsyncLazy(Func<Task<TResult>> factory)
{
_factory = factory ?? throw new ArgumentNullException(nameof(factory));
}
public Task<TResult> Task
{
get
{
var currentTask = Volatile.Read(ref _task);
if (currentTask == null)
{
Task<TResult> newTask = null;
var newTaskTask = new Task<Task<TResult>>(async () =>
{
try
{
var result = await _factory().ConfigureAwait(false);
_factory = null; // No longer needed (let it get recycled)
return result;
}
catch
{
_ = Interlocked.CompareExchange(ref _task, null, newTask);
throw;
}
});
newTask = newTaskTask.Unwrap();
currentTask = Interlocked
.CompareExchange(ref _task, newTask, null) ?? newTask;
if (currentTask == newTask)
newTaskTask.RunSynchronously(TaskScheduler.Default);
}
return currentTask.IsCompleted ?
currentTask : RunContinuationsAsynchronously(currentTask);
}
}
public TaskAwaiter<TResult> GetAwaiter() { return this.Task.GetAwaiter(); }
private static Task<TResult> RunContinuationsAsynchronously(Task<TResult> task)
{
return task.ContinueWith(t => t,
default, TaskContinuationOptions.RunContinuationsAsynchronously,
TaskScheduler.Default).Unwrap();
}
}
用法示例:
var deferredTask = new AsyncLazy<string>(async () =>
{
return await _httpClient.GetStringAsync("https://stackoverflow.com");
});
//... (the operation has not started yet)
string html = await deferredTask;