Как инициализировать объект, используя шаблон async-wait

Я пытаюсь следовать шаблону 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 вычисляется асинхронно, класс не может быть полностью инициализирован в конструкторе. Может быть, правильно иметь это значение как нуль изначально, но тогда мне нужно иметь место, где оно будет вычисляться в первый раз. Естественным местом для этого может быть геттер ImportantValue, но я нацелен на то, чтобы сделать его потокобезопасным и без побочных эффектов.

Поэтому я в основном придерживаюсь этих противоречий. Не могли бы вы помочь мне и предложить альтернативу? Наличие значения, инициализированного в конструкторе, в то время как славное на самом деле не является необходимым, но никаких побочных эффектов и безопасности потоков не является обязательным.

Спасибо заранее.

EDIT: Еще одна вещь, которую нужно добавить. Я использую Ninject для создания экземпляра, и, насколько я понимаю, он не поддерживает методы async для создания привязки. Хотя подход с инициированием некоторых операций на основе задач в конструкторе будет работать, я не могу дождаться его результата.

т.е. два следующих подхода (предлагаемые в качестве ответов до сих пор) не будут компилироваться, поскольку возвращается Задача, а не мой объект:

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

или

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

Простое связывание будет работать, но я не ожидаю результата асинхронной инициализации, запущенного в конструкторе, как было предложено Стивеном Клири:

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

... и это не выглядит хорошо для меня.

Ответ 1

У меня есть сообщение в блоге, в котором описывается несколько подходов к построению async.

Я рекомендую асинхронный метод factory, как описано Ридом, но иногда это невозможно (например, инъекция зависимостей). В этих случаях вы можете использовать асинхронный шаблон инициализации следующим образом:

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

Ответ 2

Один потенциальный вариант - переместить это в метод factory вместо использования конструктора.

Затем ваш метод factory может вернуть Task<ServiceProvider>, что позволит вам выполнить инициализацию асинхронно, но не вернуть построенный ServiceProvider, пока не будет (асинхронно) вычисляться ImportantValue.

Это позволит вашим пользователям писать код, например:

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

Ответ 3

Я знаю, что это старый вопрос, но он первый, который появляется в Google, и, честно говоря, принятый ответ - плохой ответ. Вы никогда не должны задерживать задержку, чтобы вы могли использовать оператор ожидания.

Лучший подход к методу инициализации:

private async Task<bool> InitializeAsync()
{
    try{
        // Initialize this instance.
    }

    catch{
        // Handle issues
        return await Task.FromResult(false);
    }

    return await Task.FromResult(true);
}

Это будет использовать инфраструктуру async для инициализации вашего объекта, но затем она вернет логическое значение.

Почему это лучший подход? Во-первых, вы не заставляете задерживать свой код, который ИМХО полностью поражает целью использования инфраструктуры async. Во-вторых, это хорошее правило, чтобы вернуть что-то из метода асинхронных вычислений. Таким образом, вы знаете, действительно ли ваш асинхронный метод работал/делал то, что предполагалось. Возвращение только задачи является эквивалентом возвращаемого значения void по неасинхронному методу.

Ответ 4

Вы можете использовать мой AsyncContainer контейнер IoC, который поддерживает тот же самый сценарий, что и вы.

Он также поддерживает другие удобные сценарии, такие как инициализаторы async, условные фабрики времени выполнения, зависят от функций async и sync factory

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

Ответ 5

Это небольшая модификация шаблона @StephenCleary инициализации async.

Разница в том, что вызывающему абоненту не нужно "помнить" await InitializationTask, или даже знать что-либо о InitializationTask (на самом деле теперь он изменен на закрытый).

Как он работает, так это то, что в каждом методе, который использует инициализированные данные, есть начальный вызов await _initializationTask. Это мгновенно возвращается во второй раз - потому что сам объект _initializationTask будет иметь логическое множество (IsCompleted, которое проверяет механизм ожидания) - поэтому не беспокойтесь об этом, инициализируя несколько раз.

Единственный улов, о котором я знаю, заключается в том, что вы не должны забывать его вызывать в каждом методе, который использует данные.

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