【问题标题】:Enforce an async method to be called once强制调用一次异步方法
【发布时间】:2015-02-05 09:18:50
【问题描述】:

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

【问题讨论】:

  • 使用Lazy,其初始化方法可以满足您的需求。这确保了初始化方法只被调用一次
  • 如果您使用此解决方案,请务必在 finally 块中调用 Release

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


【解决方案1】:

我会选择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 Toub 的 AsyncLazy .NET 4.0 实现。他描述了为什么您需要这样做,而不仅仅是在初始化期间调用异步方法。
  • @PanagiotisKanavos 我知道。这是从他在 PFX 团队的帖子中复制的。我已经添加了链接:)
  • Stephen Cleary 为 .NET 4.5 更新了这个并取消了 here
  • @PanagiotisKanavos 它们部分相同,只是用Task.Run 更新。我已经更新了代码。
【解决方案2】:

是的。使用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;
    // ...
}

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

【讨论】:

    【解决方案3】:

    我有一个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&lt;T&gt; 是定义共享异步资源的一种非常好的方法(并且可能是“服务”的更好概念匹配,具体取决于它的使用方式)。异步工厂方法方法的一个优点是无法创建 MyService 的未初始化版本。

    【讨论】:

    • +1,工厂方法绝对是在这种情况下要走的路。我建议使工厂成为非静态的,以便可以轻松地模拟和注入依赖项。
    【解决方案4】:

    Stephen Toub 的 AsyncLazy&lt;T&gt; 实现非常简洁明了,但有几点我不喜欢:

    1. 如果异步操作失败,错误将被缓存,并将传播到AsyncLazy&lt;T&gt; 实例的所有未来等待者。没有办法取消缓存缓存的Task,这样异步操作就可以重试了。

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

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

    4. Lazy&lt;Task&lt;T&gt;&gt; 组合在最新版本的 Visual Studio 2019 (16.8.2) 中生成 warnings。看来这种组合can produce deadlocks在某些场景下。

    5. 如果作为参数传递给Lazy&lt;Task&lt;T&gt;&gt; 构造函数的异步委托没有正确的异步实现,而是阻塞调用线程,那么所有将await Lazy&lt;Task&lt;T&gt;&gt; 实例的线程都将被阻塞,直到完成委托。这是Lazy&lt;T&gt; 类型如何工作的直接结果。这种类型从未设计用于以任何方式支持异步操作。

    Stephen Cleary 的AsyncLazy&lt;T&gt; implementationAsyncEx 库的一部分)解决了第一个问题,它在其构造函数中接受RetryOnFailure 标志。第二个问题也已通过相同的实现(ExecuteOnCallingThread 标志)得到解决。 AFAIK 第三、第四和第五个问题尚未解决。

    以下是解决所有这些问题的尝试。此实现不是基于 Lazy&lt;Task&lt;T&gt;&gt;,而是在内部使用瞬态嵌套任务 (Task&lt;Task&lt;T&gt;&gt;) 作为包装器。

    /// <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;
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-12-11
      • 1970-01-01
      • 1970-01-01
      • 2023-04-07
      • 1970-01-01
      • 2016-07-20
      相关资源
      最近更新 更多