Асинхронная инициализация компонентов

в 7:18, , рубрики: .net, ioc контейнеры, unity, асинхронное программирование, Программирование

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

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

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

Если представить инициализацию компонентов в виде графика, то порядок загрузки будет такой:

Асинхронная инициализация компонентов - 1

Очевидно, что первые три компонента можно инициализировать асинхронно, а в последнем ожидать результат через 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.

Асинхронная инициализация компонентов - 2

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

        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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js