强制异步方法只能被调用一次

15

假设我有一个类需要使用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,并且该服务将被注入,因此将其作为“Lazy”资源消耗或使用异步工厂不适用于我(尽管在其他用例中可能很好)。因此,异步初始化应封装在类内部,并对IMyService使用者透明。

将初始化代码包装在“虚拟”的AsyncLazy<>对象中的想法可以完成任务,尽管对我来说感觉有点不自然。


使用一个Lazy,其初始化方法执行你想要的操作。这样可以确保初始化方法只被调用一次。 - Panagiotis Kanavos
1
如果您选择这个解决方案,请确保在 finally 块中调用 Release - Daniel Kelley
这段代码有什么问题吗?还是说只是为了简化代码?基本上我不能在我的代码中使用Lazy - wilmol
4个回答

15
我会选择 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。他描述了为什么需要这样做,而不是在初始化期间直接调用异步方法。 - Panagiotis Kanavos
@PanagiotisKanavos 我知道。这是从他在PFX团队的帖子中复制的。我已经添加了链接 :) - Yuval Itzchakov
1
Stephen Cleary 更新了这个针对 .NET 4.5 的内容,并且取消了解包装的操作 在这里 - Panagiotis Kanavos
@PanagiotisKanavos 它们基本相同,只是使用 Task.Run 进行更新。我已经更新了代码。 - Yuval Itzchakov
为什么要使用Task.Run? - Rookian

7

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

如果您更喜欢微软的实现方式,可以使用Visual Studio SDK的AsyncLazy

6

我有一篇博客文章,介绍了几种“异步构造函数”的选择。

通常,我更喜欢异步工厂方法,因为我认为它们更简单且更安全:

public class MyService
{
  private MyService() { }

  public static async Task<MyService> CreateAsync()
  {
    var result = new MyService();
    result.Value = await ...;
    return result;
  }
}

AsyncLazy<T> 是定义共享异步资源的一种很好的方式(根据使用方式,可能更符合“服务”的概念匹配)。异步工厂方法的一个优点是不可能创建未初始化的 MyService 版本。


+1,在这种情况下,工厂方法绝对是可行的方式。我建议将工厂设置为非静态,以便轻松进行模拟和依赖注入。 - Doctor Jones

2

Stephen Toub的AsyncLazy<T>实现基于Lazy<Task<T>>,非常好且简洁,但是有一些问题并不完全符合我的喜好:

  1. 如果异步操作失败,则会缓存错误,并将其传播到AsyncLazy<T>实例的所有未来等待者。没有办法取消已缓存的Task,以便可以重试异步操作。例如,这使得AsyncLazy<T>在实现缓存系统时几乎无法使用。

  2. 异步委托在ThreadPool上调用。没有办法在调用线程上调用它。

  3. 如果我们尝试通过直接调用taskFactory委托而不是将其包装在Task.Factory.StartNew中来解决前一个问题,则在不幸的情况下,该委托阻塞调用线程相当长的时间,所有等待AsyncLazy<T>实例的线程都将被阻塞,直到委托完成。这是Lazy<T>类型如何工作的直接结果。该类型从未设计为以任何方式支持异步操作。

  4. Lazy<Task<T>>组合在Visual Studio 2019(16.8.2)的最新版本中会生成警告。似乎这种组合可能会在某些情况下导致死锁

Stephen Cleary的AsyncLazy<T>实现(AsyncEx库的一部分)解决了第一个问题,其构造函数接受一个RetryOnFailure标志。同样的实现也解决了第二个问题(ExecuteOnCallingThread标志)。据我所知,第三和第四个问题尚未得到解决。

以下是尝试解决所有这些问题的一种方法。这个实现不是基于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上调用它,您可以更改实现,用Start方法替换RunSynchronously,或将taskFactory包装在Task.Run中(例如上面的示例中的new AsyncLazy<string>(() => Task.Run(async () =>)。通常情况下,异步委托期望快速返回,因此在调用线程上调用它不应该是一个问题。作为一个奖励,它打开了从委托内部与线程相关的组件(如UI控件)交互的可能性。
这个实现传播所有可能由taskFactory委托引发的异常,而不仅仅是第一个异常。在一些情况下,这可能很重要,比如当委托直接返回一个Task.WhenAll任务时。要实现这个,首先将AsyncLazy<T>.Task存储在一个变量中,然后在catch块中检查变量的Exception.InnerExceptions属性。
可以在这里找到AsyncLazy<T>类的在线演示。它演示了多个并发工作者使用该类时的行为,以及taskFactory失败的情况。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接