Многие приложения весьма долго стартуют из-за того, что инициализация тяжелых компонентов требует времени на загрузку данных. В какой-то момент возникло логичное желание сократить время старта за счет асинхронного выполнения части операций.
Под приложением я сейчас имею ввиду довольно «толстый» бекенд некоего интернет-сервиса, которому для старта необходимо подгрузить немало всяких бизнес-кешей до того, как нода попадет в балансировщик нагрузки, избавляя первых пришедших пользователей от томительного ожидания, а дежурного администратора от алерта о том, что приложение отвечает слишком медленно.
Асинхронную логику я решил реализовывать через механизм 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. Я было взялся сперва описывать детально, почему я использую один подход вместо другого для каждого куска кода, но получился совсем уж сумбурный поток сознания не по теме, поэтому я все это стер и буду рад конкретным вопросам.
Автор: fcoder