非同期コンポーネントの初期化

重いコンポーネントの初期化にはデータのダウンロードに時間がかかるため、多くのアプリケーションは非常に長時間起動します。 ある時点で、一部の操作が非同期に実行されるため、開始時間を短縮するという論理的な要望がありました。



アプリケーションとは、特定のインターネットサービスのかなり「厚い」バックエンドを意味します。これはノードがロードバランサーに入る前に多くの種類のビジネスキャッシュをロードする必要があり、最初の着信ユーザーを苦痛から解放し、管理者がアプリケーションの応答が遅すぎること。



私は非同期/待機メカニズムを介して非同期ロジックを実装し、 Unityにすぐに使えるコンポーネントを登録することにしました。



アプリケーションには、長い初期化が必要な4つの重いコンポーネントがあります。 さらに、最初の3つがすでに作業の準備ができている場合にのみ、4つ目の初期化を実行できます。

インターフェース
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); //    } }
      
      





通常、アプリケーションの起動時のコンポーネントは次のようにロードされます。コンストラクターが呼び出され、必要に応じてコンポーネントが初期化され、最後にコンポーネントの完成したインスタンス(インスタンス)がコンテナーに登録されます



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





コンポーネントの初期化をグラフの形式で表す場合、読み込み順序は次のようになります。







明らかに、最初の3つのコンポーネントは非同期で初期化でき、後者では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は、並列スレッドで初期化タスクを実行します。 したがって、ここでは、非同期性とともに実行の並列性が使用されます。 すべてのコンポーネントに非同期バージョンがあるわけではないため、これはプラスです。 このため、この操作はスレッドセーフではないため、コンテナにインターフェイスを登録するためのロックが追加されました。 初期化操作に非同期が必要な場合、TaskでTask.Runオーバーロードを使用し、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のプロジェクトコード全体。 わかりやすくするために、少しログを追加しました。



PS私は最初に、各コードに対して別のアプローチではなくあるアプローチを使用する理由を詳細に説明することを引き受けましたが、トピックから完全に混oticとした意識の流れが得られたため、これをすべて消去し、特定の質問に喜んでいます。



All Articles