强制调用一次异步方法

问题描述 投票:0回答:4

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

c# .net asynchronous async-await task-parallel-library
4个回答
14
投票

我会选择

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


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

或者如果您更喜欢 Microsoft 实现,则可以使用 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
的未初始化版本。


2
投票

Stephen Toub 的

AsyncLazy<T>
实现,基于
Lazy<Task<T>>
,非常漂亮和简洁,但有一些事情并不完全符合我的喜好:

  1. 如果异步操作失败,错误将被缓存,并将传播到

    AsyncLazy<T>
    实例的所有未来等待者。没有办法取消缓存已缓存的
    Task
    ,以便重试异步操作。例如,这使得
    AsyncLazy<T>
    实际上无法用于实现缓存系统。

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

  3. 如果我们尝试通过直接调用

    taskFactory
    委托而不是将其包装在
    Task.Factory.StartNew
    中来解决前面的问题,那么在不幸的情况下,委托会阻塞调用线程很长一段时间,所有将
    await
    AsyncLazy<T>
    实例将被阻塞,直到委托完成。这是
    Lazy<T>
    类型工作原理的直接结果。这种类型从来都不是为了以任何方式支持异步操作而设计的。

  4. Lazy<Task<T>>
    组合在最新版本的 Visual Studio 2019 (16.8.2) 中生成警告。看来这个组合在某些场景下会产生死锁

第一个问题已由 Stephen Cleary 的

AsyncLazy<T>
implementationAsyncEx 库的一部分)解决,它在其构造函数中接受
RetryOnFailure
标志。第二个问题也已通过相同的实现得到解决(
ExecuteOnCallingThread
标志)。 AFAIK 第三和第四个问题还没有解决。

以下是解决所有这些问题的尝试。此实现不是基于

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
上调用它,您可以更改实现并将
RunSynchronously
替换为
Start
方法,或者将
taskFactory
包装在
Task.Run
中(
new AsyncLazy<string>(() => Task.Run(async () =>
中)上面的例子)。通常,异步委托预计会快速返回,因此在调用线程上调用它应该不成问题。作为奖励,它提供了从委托内部与线程仿射组件(例如 UI 控件)进行交互的可能性。

此实现传播

taskFactory
委托可能引发的所有异常,而不仅仅是第一个。这在某些情况下可能很重要,例如当委托直接返回
Task.WhenAll
任务时。为此,首先将
AsyncLazy<T>.Task
存储在变量中,然后将
await
存储在变量中,最后在
catch
块中检查变量的
Exception.InnerExceptions
属性。

可以在

这里
找到AsyncLazy<T>课程的在线演示。它演示了该类被多个并发工作线程使用时的行为,并且
taskFactory
失败了。

© www.soinside.com 2019 - 2024. All rights reserved.