Enforce an async method to be called once

2020-02-25 08:08发布

Say I have a class that needs to perform some async initialization using an InitializeAsync() method. I want to make sure that the initialization is performed only once. If another thread calls this method while the initalization is already in progress, it will "await" until the first call returns.

I was thinking about the following imlementation (using SemaphoreSlim). Is there a better/simpler approach?

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);
        });
    }
}

Thanks!

Edit:

Since I'm using DI and this service will be injected, consuming it as a "Lazy" resource or using an async factory won't work for me (although it could be great in other use cases). Thus, the async initialization should be encapsulated within the class and transparent to the IMyService consumers.

The idea of wrapping the initialization code in a "dummy" AsyncLazy<> object will do the job, although it feels a bit unnatural to me.

3条回答
女痞
2楼-- · 2020-02-25 08:37

I'd go with AsyncLazy<T> (slightly modified version):

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(); } 
}

And consume it like this:

private AsyncLazy<bool> asyncLazy = new AsyncLazy<bool>(async () =>
                                    { 
                                        await DoStuffOnlyOnceAsync()
                                        return true;
                                    });

Note i'm using bool simply because you have no return type from DoStuffOnlyOnceAsync.

Edit:

Stephan Cleary (of course) also has an implementation of this here.

查看更多
Deceive 欺骗
3楼-- · 2020-02-25 08:41

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

Or the visual studio SDK's AsyncLazy if you prefer a Microsoft implementation.

查看更多
爷的心禁止访问
4楼-- · 2020-02-25 08:44

I have a blog post that covers a few different options for doing "asynchronous constructors".

Normally, I prefer asynchronous factory methods, because I think they're simpler and a bit safer:

public class MyService
{
  private MyService() { }

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

AsyncLazy<T> is a perfectly good way of defining a shared asynchronous resource (and may be a better conceptual match for a "service", depending on how it is used). The one advantage of the async factory method approach is that it's not possible to create an uninitialized version of MyService.

查看更多
登录 后发表回答