强制执行一次异步方法

时间:2015-02-05 09:18:50

标签: c# .net asynchronous async-await task-parallel-library

假设我有一个需要使用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<>对象中的想法将完成这项工作,尽管对我来说感觉有点不自然。

4 个答案:

答案 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的返回类型。

修改

Cleary(当然)也有这个here的实现。

答案 1 :(得分:7)

是。使用Stephen Cleary's AsyncLazyAsyncEx 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> 实现非常简洁,但有几点我不喜欢:

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

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

  3. 当异步委托完成时,如果附加了多个延续,所有延续将在同一个线程上同步调用。因此,如果一个异步工作流的 await asyncLazy 后跟一些冗长/阻塞的代码,则所有其他等待相同 asyncLazy 的异步工作流都会受到影响(延迟)。此问题特别影响 .NET Framework。对于 .NET Core 和 .NET 5 来说,这不是问题,因为在这些平台上,(非完整)异步生成任务的延续是异步运行的。这不是一个高严重性问题,因为它只影响在完成之前await AsyncLazy<T> 的工作流。

  4. Lazy<Task<T>> 组合在最新版本的 Visual Studio 2019 (16.8.2) 中生成 warnings。在某些情况下,这种组合似乎 can produce deadlocks

Stephen Cleary 的 AsyncLazy<T> implementationAsyncEx 库的一部分)解决了第一个问题,它在其构造函数中接受 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;