[Из песочницы] Асинхронная инициализация компонентов
Многие приложения весьма долго стартуют из-за того, что инициализация тяжелых компонентов требует времени на загрузку данных. В какой-то момент возникло логичное желание сократить время старта за счет асинхронного выполнения части операций.Под приложением я сейчас имею ввиду довольно «толстый» бекенд некоего интернет-сервиса, которому для старта необходимо подгрузить немало всяких бизнес-кешей до того, как нода попадет в балансировщик нагрузки, избавляя первых пришедших пользователей от томительного ожидания, а дежурного администратора от алерта о том, что приложение отвечает слишком медленно.
Асинхронную логику я решил реализовывать через механизм async/await, а готовые к работе компоненты регистрировать в Unity.Пусть в приложении будет четыре тяжелых компонента, требующих долгой инициализации. Причем четвертый может начать выполнять свою инициализацию только когда первые три уже готовы к работе.
Интерфейсы public interface IComponent1 { }
public interface IComponent2 { }
public interface IComponent3 { }
public interface IComponent4 { } Реализация public class HeavyComponent1: IComponent1 { public void Initialize (int initializationDelaySeconds) { Thread.Sleep (1000 * initializationDelaySeconds); // блокирует вызывающий поток } }
public class HeavyComponent2: IComponent2 { public void Initialize (int initializationDelaySeconds) { Thread.Sleep (1000 * initializationDelaySeconds); // блокирует вызывающий поток } }
public class HeavyComponent3: IComponent3 { public void Initialize (int initializationDelaySeconds) { Thread.Sleep (1000 * initializationDelaySeconds); // блокирует вызывающий поток } }
public class HeavyComponent4: IComponent4 { public HeavyComponent4(IComponent1 componentInstance1, IComponent2 componentInstance2, IComponent3 componentInstance3) { // Требуются готовые экземпляры трех предыдущих компонентов для вызова конструктора }
public void Initialize (int initializationDelaySeconds)
{
Thread.Sleep (1000 * initializationDelaySeconds); // блокирует вызывающий поток
}
}
Компоненты при старте приложения обычно загружаются примерно следующим образом: вызывается конструктор, затем, если это необходимо, выполняется инициализация компонента, и наконец, готовый экземпляр компонента (instance) регистрируется в контейнере.
public void RegisterComponents ()
{
var heavyComponent1 = new HeavyComponent1();
heavyComponent1.Initialize (1);
this.RegisterInstance
var heavyComponent2 = new HeavyComponent2();
heavyComponent2.Initialize (2);
this.RegisterInstance
var heavyComponent3 = new HeavyComponent3();
heavyComponent3.Initialize (3);
this.RegisterInstance
var heavyComponent4 = new HeavyComponent4(heavyComponent1, heavyComponent2, heavyComponent3);
heavyComponent4.Initialize (4);
this.RegisterInstance
Очевидно, что первые три компонента можно инициализировать асинхронно, а в последнем ожидать результат через await:
public async Task RegisterAsync () { var syncReg = new Object ();
var heavyComponent1Task = Task.Run (() =>
{
var heavyComponent1 = new HeavyComponent1();
heavyComponent1.Initialize (1);
lock (syncReg)
{
this.RegisterInstance
var heavyComponent2Task = Task.Run (() =>
{
var heavyComponent2 = new HeavyComponent2();
heavyComponent2.Initialize (2);
lock (syncReg)
{
this.RegisterInstance
var heavyComponent3Task = Task.Run (() =>
{
var heavyComponent3 = new HeavyComponent3();
heavyComponent3.Initialize (3);
lock (syncReg)
{
this.RegisterInstance
var heavyComponent4Task = Task.Run (async () =>
{
var heavyComponent4 = new HeavyComponent4(await heavyComponent1Task, await heavyComponent2Task, await heavyComponent3Task);
heavyComponent4.Initialize (4);
lock (syncReg)
{
this.RegisterInstance
await Task.WhenAll (heavyComponent1Task, heavyComponent2Task, heavyComponent3Task, heavyComponent4Task); } Теперь инициализация выглядит как на картинке ниже. Task.Run будет запускать задачи инициализации в параллельных потоках. Поэтому тут вместе с асинхронностью будет использоваться параллельность выполнения. Это даже плюс, так как далеко не все компоненты имеют асинхронные версии. Из-за этого добавлена блокировка (lock) на регистрацию интерфейса в контейнере, потому что эта операция не потокобезопасна. Когда операция инициализации требует асинхронности, просто используем перегрузку Task.Run с Task в качестве параметра, которая корректно работает с async/await.
Чтобы не писать одно и тоже для каждого компонента, напишем пару методов для удобства:
private Task
return instance; });
_registrationTasks.Add (result); // потокобезопасный контейнер для всех задач регистрации return result; }
private Task
var heavyComponent2Task = RegisterInstanceAsync
var heavyComponent3Task = RegisterInstanceAsync
var heavyComponent4Task = RegisterInstanceAsync
await FinishRegistrationTasks (); } Код проекта целиком на github. Я добавил немного логгирования для наглядности.
P.S. Я было взялся сперва описывать детально, почему я использую один подход вместо другого для каждого куска кода, но получился совсем уж сумбурный поток сознания не по теме, поэтому я все это стер и буду рад конкретным вопросам.