如何使用async-await模式初始化对象

本文关键字:初始化 对象 模式 async-await 何使用 | 更新日期: 2023-09-27 18:08:22

我试图在我的服务类中遵循RAII模式,这意味着当一个对象被构造时,它被完全初始化。然而,我在异步api方面遇到了困难。所讨论的类的结构如下

class ServiceProvider : IServiceProvider // Is only used through this interface
{
    public int ImportantValue { get; set; }
    public event EventHandler ImportantValueUpdated;
    public ServiceProvider(IDependency1 dep1, IDependency2 dep2)
    {
        // IDependency1 provide an input value to calculate ImportantValue
        // IDependency2 provide an async algorithm to calculate ImportantValue
    }
}

我还致力于消除ImportantValue getter中的副作用,使其线程安全。

现在ServiceProvider的用户将创建它的实例,订阅ImportantValue变化的事件,并获得初始的ImportantValue。问题来了,关于初值。由于ImportantValue是异步计算的,类不能在构造函数中完全初始化。最初将这个值设为null可能是可以的,但之后我需要在某个地方第一次计算它。一个自然的地方可能是ImportantValue的getter,但我的目标是使它线程安全,没有副作用。

所以我基本上被这些矛盾困住了。你能帮助我并提供一些替代方案吗?在构造函数中初始化值虽然很好,但没有副作用,并且属性的线程安全性是强制性的。

提前感谢。

EDIT:还有一件事要添加。我正在使用Ninject进行实例化,据我所知,它不支持异步方法来创建绑定。虽然在构造函数中启动一些基于任务的操作的方法可以工作,但我不能等待它的结果。

。接下来的两种方法(到目前为止作为答案提供)将无法编译,因为返回的是Task,而不是my object:

Kernel.Bind<IServiceProvider>().ToMethod(async ctx => await ServiceProvider.CreateAsync())

Kernel.Bind<IServiceProvider>().ToMethod(async ctx => 
{
    var sp = new ServiceProvider();
    await sp.InitializeAsync();
})

简单的绑定可以工作,但我不等待在构造函数中开始的异步初始化的结果,正如Stephen Cleary所建议的:

Kernel.Bind<IServiceProvider>().To<ServiceProvider>();

…这对我来说可不太好。

如何使用async-await模式初始化对象

我有一篇博客文章描述了几种构建async的方法。

我推荐Reed描述的异步工厂方法,但有时这是不可能的(例如,依赖注入)。在这些情况下,您可以使用如下的异步初始化模式:

public sealed class MyType
{
    public MyType()
    {
        Initialization = InitializeAsync();
    }
    public Task Initialization { get; private set; }
    private async Task InitializeAsync()
    {
        // Asynchronously initialize this instance.
        await Task.Delay(100);
    }
}
然后可以正常构造该类型,但请记住,只有构造才会启动异步初始化。当需要初始化类型时,代码可以这样做:
await myTypeInstance.Initialization;

注意,如果Initialization已经完成,执行(同步)将继续超过await


如果你想要一个真正的异步属性,我也有一篇博文。您的情况听起来可能会受益于AsyncLazy<T>:

public sealed class MyClass
{
    public MyClass()
    {
        MyProperty = new AsyncLazy<int>(async () =>
        {
            await Task.Delay(100);
            return 13;
        });
    }
    public AsyncLazy<int> MyProperty { get; private set; }
}

一个可能的选择是将其移到工厂方法中,而不是使用构造函数。

您的工厂方法可以返回一个Task<ServiceProvider>,这将允许您异步执行初始化,但在(异步)计算ImportantValue之前不会返回构造的ServiceProvider

这将允许您的用户编写如下代码:

var sp = await ServiceProvider.CreateAsync();
int iv = sp.ImportantValue; // Will be initialized at this point

这是对@StephenCleary异步初始化模式的一个轻微修改。

不同之处在于调用者不需要"记住"awaitInitializationTask,甚至不需要知道initializationTask的任何信息(实际上它现在被更改为private)。

它的工作方式是在每个使用初始化数据的方法中都有对await _initializationTask的初始调用。这将立即返回第二次-因为_initializationTask对象本身将有一个布尔集合(IsCompleted由'await'机制检查)-所以不用担心它初始化多次。

我所知道的唯一问题是你不能忘记在使用数据的每个方法中调用它。

public sealed class MyType
{
    public MyType()
    {
        _initializationTask = InitializeAsync();
    }
    private Task _initializationTask;
    private async Task InitializeAsync()
    {
        // Asynchronously initialize this instance.
        _customers = await LoadCustomersAsync();
    }
    public async Task<Customer> LookupCustomer(string name)
    {
         // Waits to ensure the class has been initialized properly
         // The task will only ever run once, triggered initially by the constructor
         // If the task failed this will raise an exception
         // Note: there are no () since this is not a method call
         await _initializationTask;
         return _customers[name];
    }
    // one way of clearing the cache
    public void ClearCache()
    {
         InitializeAsync();
    }
    // another approach to clearing the cache, will wait until complete
    // I don't really see a benefit to this method since any call using the
    // data (like LookupCustomer) will await the initialization anyway
    public async Task ClearCache2()
    {
         await InitializeAsync();
    }
 }

您可以使用我的AsyncContainer IoC容器,它支持与您完全相同的场景。

它还支持其他方便的场景,如异步初始化器,运行时条件工厂,依赖于异步和同步工厂函数

//The email service factory is an async method
public static async Task<EmailService> EmailServiceFactory() 
{
  await Task.Delay(1000);
  return new EmailService();
}
class Service
{
     //Constructor dependencies will be solved asynchronously:
     public Service(IEmailService email)
     {
     }
} 
var container = new Container();
//Register an async factory:
container.Register<IEmailService>(EmailServiceFactory);
//Asynchronous GetInstance:
var service = await container.GetInstanceAsync<Service>();
//Safe synchronous, will fail if the solving path is not fully synchronous:
var service = container.GetInstance<Service>();

我知道这是一个古老的问题,但它是第一个出现在谷歌上,坦率地说,公认的答案是一个糟糕的答案。永远不要为了使用await操作符而强制延迟。

初始化方法的更好方法:

private async Task<bool> InitializeAsync()
{
    try{
        // Initialize this instance.
    }
    catch{
        // Handle issues
        return await Task.FromResult(false);
    }
    return await Task.FromResult(true);
}

这将使用异步框架来初始化你的对象,但随后它将返回一个布尔值。

为什么这是一个更好的方法?首先,你没有强迫代码延迟,我认为这完全违背了使用异步框架的目的。其次,从异步方法返回一些东西是一条很好的经验法则。这样,您就知道您的async方法是否实际工作/做了它应该做的事情。只返回Task就相当于在非异步方法上返回void。

我有一个Stephen Cleary的异步初始化模式示例的变体。您可以封装Initialization属性并在类方法中等待它。在这种情况下,客户端代码将不需要等待初始化任务。

public class ClassWithAsyncInit
{
    public ClassWithAsyncInit()
    {
        Initialization = InitializeAsync();
    }
    private Task Initialization { get; private set; }
    private async Task InitializeAsync()
    {
    // your async init code
    }
    public async Task FirstMethod()
    {
        await Initialization;
        // ... other code
    }
}

缺点是,如果您的类中有很多方法,并且需要等待每个方法中的Initialization任务,则不方便。但有时也没关系。假设您有一个用于保存JSON对象的简单接口:

public IDataSaver
{
    void Save(string json);
}

您需要使用异步初始化逻辑为数据库实现它。考虑到您将只有一个公共方法,封装Initialization属性并在Save方法中等待它是有意义的:

public class SomeDbDataSaver: IDataSaver
{
    protected DatabaseClient DbClient { get; set; }
    public SomeDbDataSaver()
    {
        DbClient = new DatabaseClient();
        Initialization = InitializeAsync(); // start off the async init
    }
    private Task Initialization { get; private set; }
    private async Task InitializeAsync()
    {
        await DbClient.CreateDatabaseIfNotExistsAsync("DatabaseName");
    }
    public async Task Save(string json)
    {
        await Initialization;
        
        // ... code for saving a data item to the database
    }
}