[Из песочницы] Асинхронная инициализация компонентов

Многие приложения весьма долго стартуют из-за того, что инициализация тяжелых компонентов требует времени на загрузку данных. В какой-то момент возникло логичное желание сократить время старта за счет асинхронного выполнения части операций.Под приложением я сейчас имею ввиду довольно «толстый» бекенд некоего интернет-сервиса, которому для старта необходимо подгрузить немало всяких бизнес-кешей до того, как нода попадет в балансировщик нагрузки, избавляя первых пришедших пользователей от томительного ожидания, а дежурного администратора от алерта о том, что приложение отвечает слишком медленно.

Асинхронную логику я решил реализовывать через механизм 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(heavyComponent1);

var heavyComponent2 = new HeavyComponent2(); heavyComponent2.Initialize (2); this.RegisterInstance(heavyComponent2);

var heavyComponent3 = new HeavyComponent3(); heavyComponent3.Initialize (3); this.RegisterInstance(heavyComponent3);

var heavyComponent4 = new HeavyComponent4(heavyComponent1, heavyComponent2, heavyComponent3); heavyComponent4.Initialize (4); this.RegisterInstance(heavyComponent1); } Если представить инициализацию компонентов в виде графика, то порядок загрузки будет такой: 6385b09be1e04760a9fbb890931f680c.png

Очевидно, что первые три компонента можно инициализировать асинхронно, а в последнем ожидать результат через await:

public async Task RegisterAsync () { var syncReg = new Object ();

var heavyComponent1Task = Task.Run (() => { var heavyComponent1 = new HeavyComponent1(); heavyComponent1.Initialize (1); lock (syncReg) { this.RegisterInstance(heavyComponent1); } return heavyComponent1; });

var heavyComponent2Task = Task.Run (() => { var heavyComponent2 = new HeavyComponent2(); heavyComponent2.Initialize (2); lock (syncReg) { this.RegisterInstance(heavyComponent2); } return heavyComponent2; });

var heavyComponent3Task = Task.Run (() => { var heavyComponent3 = new HeavyComponent3(); heavyComponent3.Initialize (3); lock (syncReg) { this.RegisterInstance(heavyComponent3); } return heavyComponent3; });

var heavyComponent4Task = Task.Run (async () => { var heavyComponent4 = new HeavyComponent4(await heavyComponent1Task, await heavyComponent2Task, await heavyComponent3Task); heavyComponent4.Initialize (4); lock (syncReg) { this.RegisterInstance(heavyComponent4); } return heavyComponent4; });

await Task.WhenAll (heavyComponent1Task, heavyComponent2Task, heavyComponent3Task, heavyComponent4Task); } Теперь инициализация выглядит как на картинке ниже. Task.Run будет запускать задачи инициализации в параллельных потоках. Поэтому тут вместе с асинхронностью будет использоваться параллельность выполнения. Это даже плюс, так как далеко не все компоненты имеют асинхронные версии. Из-за этого добавлена блокировка (lock) на регистрацию интерфейса в контейнере, потому что эта операция не потокобезопасна. Когда операция инициализации требует асинхронности, просто используем перегрузку Task.Run с Task в качестве параметра, которая корректно работает с async/await.25303c3b22b645028d99e5f3bf4be3d7.png

Чтобы не писать одно и тоже для каждого компонента, напишем пару методов для удобства:

private Task RegisterInstanceAsync(Func registration) { var result = Task.Run (() => { var instance = registration (); lock (_syncReg) { this.RegisterInstance (instance); }

return instance; });

_registrationTasks.Add (result); // потокобезопасный контейнер для всех задач регистрации return result; }

private Task RegisterInstanceAsync(Func> registration) { return RegisterInstanceAsync (() => registration ().Result); } Здесь _registrationTasks — потокобезопасный контейнер (я использовал ConcurrentBag), чтобы потом явно дождаться завершения всех задач инициализации: private async Task FinishRegistrationTasks () { await Task.WhenAll (_registrationTasks); } Теперь код асинхронной инициализации компонентов выглядит просто и наглядно: public async Task RegisterComponentsAsync () { var heavyComponent1Task = RegisterInstanceAsync(() => { var result = new HeavyComponent1(); result.Initialize (1); return result; });

var heavyComponent2Task = RegisterInstanceAsync(() => { var result = new HeavyComponent2(); result.Initialize (2); return result; });

var heavyComponent3Task = RegisterInstanceAsync(() => { var result = new HeavyComponent3(); result.Initialize (3); return result; });

var heavyComponent4Task = RegisterInstanceAsync(async () => { var result = new HeavyComponent4(await heavyComponent1Task, await heavyComponent2Task, await heavyComponent3Task); result.Initialize (4); return result; });

await FinishRegistrationTasks (); } Код проекта целиком на github. Я добавил немного логгирования для наглядности.

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

© Habrahabr.ru