📜 ⬆️ ⬇️

Asynchronous component initialization

Many applications start very long due to the fact that the initialization of heavy components takes time to load data. At some point, there was a logical desire to reduce the start time due to the asynchronous execution of part of the operations.

By application, I now mean a rather “thick” backend of a certain Internet service, which, to start, needs to load a lot of business caches before the node gets into the load balancer, saving the first incoming users from anxious waiting, and the administrator on duty that the application is responding too slowly.

I decided to implement asynchronous logic through the async / await mechanism, and register the components that are ready for work with Unity .

Let the application be four heavy components that require long initialization. Moreover, the fourth one can start performing its initialization only when the first three are ready for operation.
Interfaces
public interface IComponent1 { } public interface IComponent2 { } public interface IComponent3 { } public interface IComponent4 { } 

Implementation
  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); //    } } 

Components at application start are usually loaded approximately as follows: the constructor is called, then, if necessary, the component is initialized, and finally, the finished instance of the component is registered in the container.
')
  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); } 

If we present the initialization of components in the form of a graph, then the boot order will be as follows:



Obviously, the first three components can be initialized asynchronously, and in the latter one can wait for the result through 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); } 

Now initialization looks like in the picture below. Task.Run will run initialization tasks in parallel threads. Therefore, parallel execution will be used here along with asynchrony. This is even a plus, since not all components have asynchronous versions. Because of this, a lock has been added (lock) on the interface registration in the container, because this operation is not thread-safe. When the initialization operation requires asynchrony, we simply use Task.Run overload with Task as a parameter that works correctly with async / await.



In order not to write the same thing for each component, let's write a couple of methods for convenience:

  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); } 

Here _registrationTasks is a thread-safe container (I used ConcurrentBag) to then explicitly wait for all initialization tasks to complete:

  private async Task FinishRegistrationTasks() { await Task.WhenAll(_registrationTasks); } 

Now the code for asynchronous initialization of components looks simple and clear:

  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(); } 


Project code entirely on github . I added some logging for clarity.

PS I took the first to describe in detail why I use one approach instead of another for each piece of code, but the confusion of consciousness is completely off topic, so I’ve erased all this and will be pleased with specific questions.

Source: https://habr.com/ru/post/247331/


All Articles