强制调用一次异步方法

Enforce an async method to be called once

假设我有一个 class 需要使用 InitializeAsync() 方法执行一些异步初始化。 我想确保只执行一次初始化。如果另一个线程在初始化过程中调用此方法,它将 "await" 直到第一次调用 returns.

我在考虑以下实现(使用 SemaphoreSlim)。 有 better/simpler 方法吗?

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 并且此服务将被注入,因此将其作为 "Lazy" 资源使用或使用异步工厂对我来说不起作用(尽管它在其他用例中可能很棒) . 因此,异步初始化应该封装在 class 中并且对 IMyService 消费者透明。

将初始化代码包装在 "dummy" 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 的 return 类型。

编辑:

Stephan Cleary(当然)也实现了这个 here

是的。使用 Stephen Cleary's AsyncLazy (available on the 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;
    // ...
}

或者 visual studio SDK's AsyncLazy 如果您更喜欢 Microsoft 实现。

我有一个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> 是定义共享异步资源的完美方式(并且可能是 "service" 的更好的概念匹配,具体取决于它的使用方式)。异步工厂方法方法的一个优点是不可能创建 MyService.

的未初始化版本

Stephen Toub 的 AsyncLazy<T> 实现非常简洁明了,但有几点我不喜欢:

  1. 如果异步操作失败,错误将被缓存,并将传播到 AsyncLazy<T> 实例的所有未来等待者。无法取消缓存已缓存的 Task,以便重试异步操作。

  2. ThreadPool 上下文中调用异步委托。无法在当前上下文中调用它。

  3. Lazy<Task<T>>组合在某些情况下生成warnings in the latest version of the Visual Studio 2019 (16.8.2). It seems that this combination

  4. 在不幸的情况下,作为参数传递给 Lazy<Task<T>> 构造函数的异步委托没有正确的异步实现,而是阻塞了调用线程,所有将 await Lazy<Task<T>> 实例将被阻塞,直到委托完成。这是 Lazy<T> 类型如何工作的直接结果。此类型从未设计用于以任何方式支持异步操作。

第一个问题已由 Stephen Cleary 的 AsyncLazy<T> implementation (part of the AsyncEx 库解决,它在其构造函数中接受 RetryOnFailure 标志。第二个问题也已通过相同的实现解决(ExecuteOnCallingThread 标志)。据我所知,第三个和第四个问题尚未解决。

以下是解决所有这些问题的尝试。此实现不是基于 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;
        }
    }

    public TaskAwaiter<TResult> GetAwaiter() { return this.Task.GetAwaiter(); }
}

用法示例:

var deferredTask = new AsyncLazy<string>(async () =>
{
    return await _httpClient.GetStringAsync("https://whosebug.com");
});

//... (the operation has not started yet)

string html = await deferredTask;

factory 委托在当前线程上被调用。如果您更喜欢在 ThreadPool 上调用它,只需替换 RunSynchronously with the Start。通常,异步委托应该很快 return,因此在当前线程上调用应该不是问题。作为奖励,它开启了从委托内部与线程仿射组件(如 UI 控件)交互的可能性。