Как инициализировать объект, используя шаблон 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();
}
}