Многие приложения весьма долго стартуют из-за того, что инициализация тяжелых компонентов требует времени на загрузку данных. В какой-то момент возникло логичное желание сократить время старта за счет асинхронного выполнения части операций.
Под приложением я сейчас имею ввиду довольно «толстый» бекенд некоего интернет-сервиса, которому для старта необходимо подгрузить немало всяких бизнес-кешей до того, как нода попадет в балансировщик нагрузки, избавляя первых пришедших пользователей от томительного ожидания, а дежурного администратора от алерта о том, что приложение отвечает слишком медленно.
Асинхронную логику я решил реализовывать через механизм async/await, а готовые к работе компоненты регистрировать в Unity.
Пусть в приложении будет четыре тяжелых компонента, требующих долгой инициализации. Причем четвертый может начать выполнять свою инициализацию только когда первые три уже готовы к работе.
Компоненты при старте приложения обычно загружаются примерно следующим образом: вызывается конструктор, затем, если это необходимо, выполняется инициализация компонента, и наконец, готовый экземпляр компонента (instance) регистрируется в контейнере.
Если представить инициализацию компонентов в виде графика, то порядок загрузки будет такой:

Очевидно, что первые три компонента можно инициализировать асинхронно, а в последнем ожидать результат через await:
Теперь инициализация выглядит как на картинке ниже. Task.Run будет запускать задачи инициализации в параллельных потоках. Поэтому тут вместе с асинхронностью будет использоваться параллельность выполнения. Это даже плюс, так как далеко не все компоненты имеют асинхронные версии. Из-за этого добавлена блокировка (lock) на регистрацию интерфейса в контейнере, потому что эта операция не потокобезопасна. Когда операция инициализации требует асинхронности, просто используем перегрузку Task.Run с Task в качестве параметра, которая корректно работает с async/await.

Чтобы не писать одно и тоже для каждого компонента, напишем пару методов для удобства:
Здесь _registrationTasks — потокобезопасный контейнер (я использовал ConcurrentBag), чтобы потом явно дождаться завершения всех задач инициализации:
Теперь код асинхронной инициализации компонентов выглядит просто и наглядно:
Код проекта целиком на github. Я добавил немного логгирования для наглядности.
P.S. Я было взялся сперва описывать детально, почему я использую один подход вместо другого для каждого куска кода, но получился совсем уж сумбурный поток сознания не по теме, поэтому я все это стер и буду рад конкретным вопросам.
Под приложением я сейчас имею ввиду довольно «толстый» бекенд некоего интернет-сервиса, которому для старта необходимо подгрузить немало всяких бизнес-кешей до того, как нода попадет в балансировщик нагрузки, избавляя первых пришедших пользователей от томительного ожидания, а дежурного администратора от алерта о том, что приложение отвечает слишком медленно.
Асинхронную логику я решил реализовывать через механизм 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<IComponent1>(heavyComponent1); var heavyComponent2 = new HeavyComponent2(); heavyComponent2.Initialize(2); this.RegisterInstance<IComponent2>(heavyComponent2); var heavyComponent3 = new HeavyComponent3(); heavyComponent3.Initialize(3); this.RegisterInstance<IComponent3>(heavyComponent3); var heavyComponent4 = new HeavyComponent4(heavyComponent1, heavyComponent2, heavyComponent3); heavyComponent4.Initialize(4); this.RegisterInstance<IComponent1>(heavyComponent1); }
Если представить инициализацию компонентов в виде графика, то порядок загрузки будет такой:

Очевидно, что первые три компонента можно инициализировать асинхронно, а в последнем ожидать результат через await:
public async Task RegisterAsync() { var syncReg = new Object(); var heavyComponent1Task = Task.Run(() => { var heavyComponent1 = new HeavyComponent1(); heavyComponent1.Initialize(1); lock (syncReg) { this.RegisterInstance<IComponent1>(heavyComponent1); } return heavyComponent1; }); var heavyComponent2Task = Task.Run(() => { var heavyComponent2 = new HeavyComponent2(); heavyComponent2.Initialize(2); lock (syncReg) { this.RegisterInstance<IComponent2>(heavyComponent2); } return heavyComponent2; }); var heavyComponent3Task = Task.Run(() => { var heavyComponent3 = new HeavyComponent3(); heavyComponent3.Initialize(3); lock (syncReg) { this.RegisterInstance<IComponent3>(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<IComponent4>(heavyComponent4); } return heavyComponent4; }); await Task.WhenAll(heavyComponent1Task, heavyComponent2Task, heavyComponent3Task, heavyComponent4Task); }
Теперь инициализация выглядит как на картинке ниже. Task.Run будет запускать задачи инициализации в параллельных потоках. Поэтому тут вместе с асинхронностью будет использоваться параллельность выполнения. Это даже плюс, так как далеко не все компоненты имеют асинхронные версии. Из-за этого добавлена блокировка (lock) на регистрацию интерфейса в контейнере, потому что эта операция не потокобезопасна. Когда операция инициализации требует асинхронности, просто используем перегрузку Task.Run с Task в качестве параметра, которая корректно работает с async/await.

Чтобы не писать одно и тоже для каждого компонента, напишем пару методов для удобства:
private Task<TInterface> RegisterInstanceAsync<TInterface>(Func<TInterface> registration) { var result = Task.Run(() => { var instance = registration(); lock (_syncReg) { this.RegisterInstance(instance); } return instance; }); _registrationTasks.Add(result); // потокобезопасный контейнер для всех задач регистрации return result; } private Task<TInterface> RegisterInstanceAsync<TInterface>(Func<Task<TInterface>> registration) { return RegisterInstanceAsync(() => registration().Result); }
Здесь _registrationTasks — потокобезопасный контейнер (я использовал ConcurrentBag), чтобы потом явно дождаться завершения всех задач инициализации:
private async Task FinishRegistrationTasks() { await Task.WhenAll(_registrationTasks); }
Теперь код асинхронной инициализации компонентов выглядит просто и наглядно:
public async Task RegisterComponentsAsync() { var heavyComponent1Task = RegisterInstanceAsync<IComponent1>(() => { var result = new HeavyComponent1(); result.Initialize(1); return result; }); var heavyComponent2Task = RegisterInstanceAsync<IComponent2>(() => { var result = new HeavyComponent2(); result.Initialize(2); return result; }); var heavyComponent3Task = RegisterInstanceAsync<IComponent3>(() => { var result = new HeavyComponent3(); result.Initialize(3); return result; }); var heavyComponent4Task = RegisterInstanceAsync<IComponent4>(async () => { var result = new HeavyComponent4(await heavyComponent1Task, await heavyComponent2Task, await heavyComponent3Task); result.Initialize(4); return result; }); await FinishRegistrationTasks(); }
Код проекта целиком на github. Я добавил немного логгирования для наглядности.
P.S. Я было взялся сперва описывать детально, почему я использую один подход вместо другого для каждого куска кода, но получился совсем уж сумбурный поток сознания не по теме, поэтому я все это стер и буду рад конкретным вопросам.