【Dart】非同期処理の最適化について考える

  • 2024年6月12日
  • 2024年6月12日
  • Dart

対象者

  • 非同期処理の最適化を考えているひと
  • なにも考えず「Future.wait」を使っている人

はじめに

現在(2024/06/11)、Singularity SocietyのSingularity Society BootCampというのに参加してます。先日オフラインイベントがあり、その中で(Windows95で右クリックを作ったことで有名な)中島聡さんの講演を聴く機会がありました。その中で中島さんは、現在の多くのプログラマは非同期処理を使いこなせていないことを言及してました。
今後のAI同士のコミュニケーションが非同期で行われていきます。そのときに各AIが並列で動作するように設計されていない場合、全体の処理に非常に時間がかかってしまうという考えでした(あれ、これは、その後の雑談の内容かな、、、)。

私自身も非同期処理を行っていますが、通常はそれほど複雑な処理を行わない(WebAPIでデータを取得して、JSONをデーコード、などのレベルです)ため、基本的に非同期を同期処理として扱うことがほとんどです。しかし、中島さんの講演を聞いて、より効率的な非同期処理の重要性を再認識しました。

そのときの要点をまとめ、Dartでの解決方法を考えてみました。もっと良い方法がある場合、ご連絡いただけると嬉しいです!

ベースのプログラム

処理としては以下の通りです

  • functionA, functionB, functionC がある
  • functionA, functionB の結果を使用する functionD がある
  • functionB, functionC の結果を使用する functionE がある
  • functionD, functionE の結果を使用する functionF がある
  • すべての処理は時間が掛かるので、非同期処理とする

結果の関係
A, B → D
B, C → E
D, E → F
B の結果に依存する関数が2つあるのがミソです

ソース(待機時間・共通)

まあ、適当。

const Duration delayA = Duration(milliseconds: 200);
const Duration delayB = Duration(milliseconds: 100);
const Duration delayC = Duration(milliseconds: 300);
const Duration delayD1 = Duration(milliseconds: 125);
const Duration delayD2 = Duration(milliseconds: 125);
const Duration delayE1 = Duration(milliseconds: 75);
const Duration delayE2 = Duration(milliseconds: 75);
const Duration delayF1 = Duration(milliseconds: 200);
const Duration delayF2 = Duration(milliseconds: 200);

引数をStringで実装

通常に作成する方法で実装します。

class SynchronousFunction {
// 非同期処理A
  Future<String> functionA() async {
    await Future.delayed(delayA);
    return 'A';
  }

// 非同期処理B
  Future<String> functionB() async {
    await Future.delayed(delayB);
    return 'B';
  }

// 非同期処理C
  Future<String> functionC() async {
    await Future.delayed(delayC);
    return 'C';
  }

// 非同期処理D: AとBの結果を使用
  Future<String> functionD(String a, String b) async {
    await Future.delayed(delayD1); // 遅延時間の適用
    final String result = a + b;
    await Future.delayed(delayD2); // 遅延時間の適用
    return result;
  }

// 非同期処理E: BとCの結果を使用
  Future<String> functionE(String b, String c) async {
    await Future.delayed(delayE1); // 遅延時間の適用
    final String result = b + c;
    await Future.delayed(delayE2); // 遅延時間の適用
    return result;
  }

// 非同期処理F: DとEの結果を使用
  Future<String> functionF(String d, String e) async {
    await Future.delayed(delayF1); // 遅延時間の適用
    final String result = d + e;
    await Future.delayed(delayF2); // 遅延時間の適用
    return result;
  }
}

非同期で処理

 test('改善前', () async {
    final target = SynchronousFunction();
    // 非同期処理A, B, Cを開始
    final String resultA = await target.functionA();
    final String resultB = await target.functionB();
    final String resultC = await target.functionC();

    // 非同期処理DとEを開始
    final String resultD = await target.functionD(resultA, resultB);
    final String resultE = await target.functionE(resultB, resultC);

    // Fの結果を待機
    final String finalResult = await target.functionF(resultD, resultE);

    // 最終結果を取得
    expect(finalResult, expectedResult);
  });

これだと毎回待つので時間が掛かります。1400ミリ秒掛かりました

同期処理を加える

  test('中途半端な改善', () async {
    final target = SynchronousFunction();

    // 非同期処理A, B, Cを開始
    final Future<String> resultA = target.functionA();
    final Future<String> resultB = target.functionB();
    final Future<String> resultC = target.functionC();

    final resultABC = await Future.wait([resultA, resultB, resultC]);

    // 非同期処理DとEを開始
    final Future<String> resultD = target.functionD(resultABC[0], resultABC[1]);
    final Future<String> resultE = target.functionE(resultABC[1], resultABC[2]);

    final resultDE = await Future.wait([resultD, resultE]);

    // Fの結果を待機
    final String finalResult = await target.functionF(resultDE[0], resultDE[1]);

    // 最終結果を取得
    expect(finalResult, expectedResult);
  });

960ミリに短縮され、改善されました。私も「これ、やるよね」と考えたのですが、これではマイクロソフトの面接で落とされるそうです(笑)
functionA,B,Cの処理時間は同じではありません。上記の書き方だと、3つの処理の一番遅い処理に時間を合わせてしまいます。理想的には必要な処理が完了したら、すぐに次の処理を続けることです。
例えば、functionCの処理に時間が掛かる場合、functionABの結果は先に取得できるので、functionDは先に処理を進めることができます。

必要な時に初めてawaitして、他のところで待機しなければ良いかと思い、書き直しました。

引数を Future型に変更

メインクラス

class FutureFunction {
// 非同期処理A
  Future<String> functionA() async {
    await Future.delayed(delayA); // 200ミリ秒の遅延
    return 'A';
  }

// 非同期処理B
  Future<String> functionB() async {
    await Future.delayed(delayB); // 300ミリ秒の遅延
    return 'B';
  }

// 非同期処理C
  Future<String> functionC() async {
    await Future.delayed(delayC); // 100ミリ秒の遅延
    return 'C';
  }

// 非同期処理D: AとBの結果を使用
  Future<String> functionD(Future<String> a, Future<String> b) async {
    await Future.delayed(delayD1); // 遅延時間の適用
    final String result = await a + await b;
    await Future.delayed(delayD2); // 遅延時間の適用
    return result;
  }

// 非同期処理E: BとCの結果を使用
  Future<String> functionE(Future<String> b, Future<String> c) async {
    await Future.delayed(delayE1); // 遅延時間の適用
    final String result = await b + await c;
    await Future.delayed(delayE2); // 遅延時間の適用
    return result;
  }

// 非同期処理F: DとEの結果を使用
  Future<String> functionF(Future<String> d, Future<String> e) async {
    await Future.delayed(delayF1); // 遅延時間の適用
    final String result = await d + await e;
    await Future.delayed(delayF2); // 遅延時間の適用
    return result;
  }
}

実行

  test('完全な改善', () async {
    final target = FutureFunction();

    // 非同期処理A, B, Cを開始
    final Future<String> resultA = target.functionA();
    final Future<String> resultB = target.functionB();
    final Future<String> resultC = target.functionC();

    // 非同期処理DとEを開始
    final Future<String> resultD = target.functionD(resultA, resultB);
    final Future<String> resultE = target.functionE(resultB, resultC);

    // Fの結果を待機
    final Future<String> finalResult = target.functionF(resultD, resultE);

    // 最終結果を取得
    expect(await finalResult, expectedResult);
  });

これで実行時間が 582ミリ秒になりました。(1400→960→582)。大分改善されました。
最初はfunctionAが時間が掛かるときと、functionCで時間が掛かるときで違うソースにしないといけないのかな、などと色々考えてましたが、引数をFuture型にして、awaitを実際の処理実行時に書くだけで、あんまり深く考えなくても大丈夫かな、と思いました。
メソッドの引数がFuture型になったので、ごちゃつくなぁ、と思いましたが、main文の方は綺麗になりました。

まとめ

非同期処理の結果を使用する非同期処理があるなど、非同期処理が複雑な場合の方針は以下の通り

  • 処理の引数をFuture型にする
  • 実際に値を使用するときにawaitを実行する

色々考えましたが、シンプルにこの2つの指針でいけそう。

ソース(main_test.dartにコピペして動作確認用)

// 遅延時間の定義
import 'package:flutter_test/flutter_test.dart';

const Duration delayA = Duration(milliseconds: 200);
const Duration delayB = Duration(milliseconds: 100);
const Duration delayC = Duration(milliseconds: 300);
const Duration delayD1 = Duration(milliseconds: 125);
const Duration delayD2 = Duration(milliseconds: 125);
const Duration delayE1 = Duration(milliseconds: 75);
const Duration delayE2 = Duration(milliseconds: 75);
const Duration delayF1 = Duration(milliseconds: 200);
const Duration delayF2 = Duration(milliseconds: 200);

class SynchronousFunction {
// 非同期処理A
  Future<String> functionA() async {
    await Future.delayed(delayA);
    return 'A';
  }

// 非同期処理B
  Future<String> functionB() async {
    await Future.delayed(delayB);
    return 'B';
  }

// 非同期処理C
  Future<String> functionC() async {
    await Future.delayed(delayC);
    return 'C';
  }

// 非同期処理D: AとBの結果を使用
  Future<String> functionD(String a, String b) async {
    await Future.delayed(delayD1); // 遅延時間の適用
    final String result = a + b;
    await Future.delayed(delayD2); // 遅延時間の適用
    return result;
  }

// 非同期処理E: BとCの結果を使用
  Future<String> functionE(String b, String c) async {
    await Future.delayed(delayE1); // 遅延時間の適用
    final String result = b + c;
    await Future.delayed(delayE2); // 遅延時間の適用
    return result;
  }

// 非同期処理F: DとEの結果を使用
  Future<String> functionF(String d, String e) async {
    await Future.delayed(delayF1); // 遅延時間の適用
    final String result = d + e;
    await Future.delayed(delayF2); // 遅延時間の適用
    return result;
  }
}

class FutureFunction {
// 非同期処理A
  Future<String> functionA() async {
    await Future.delayed(delayA); // 200ミリ秒の遅延
    return 'A';
  }

// 非同期処理B
  Future<String> functionB() async {
    await Future.delayed(delayB); // 300ミリ秒の遅延
    return 'B';
  }

// 非同期処理C
  Future<String> functionC() async {
    await Future.delayed(delayC); // 100ミリ秒の遅延
    return 'C';
  }

// 非同期処理D: AとBの結果を使用
  Future<String> functionD(Future<String> a, Future<String> b) async {
    await Future.delayed(delayD1); // 遅延時間の適用
    final String result = await a + await b;
    await Future.delayed(delayD2); // 遅延時間の適用
    return result;
  }

// 非同期処理E: BとCの結果を使用
  Future<String> functionE(Future<String> b, Future<String> c) async {
    await Future.delayed(delayE1); // 遅延時間の適用
    final String result = await b + await c;
    await Future.delayed(delayE2); // 遅延時間の適用
    return result;
  }

// 非同期処理F: DとEの結果を使用
  Future<String> functionF(Future<String> d, Future<String> e) async {
    await Future.delayed(delayF1); // 遅延時間の適用
    final String result = await d + await e;
    await Future.delayed(delayF2); // 遅延時間の適用
    return result;
  }
}

// メイン関数
Future<void> main() async {
  const expectedResult = 'ABBC';

  test('改善前', () async {
    final target = SynchronousFunction();
    // 非同期処理A, B, Cを開始
    final String resultA = await target.functionA();
    final String resultB = await target.functionB();
    final String resultC = await target.functionC();

    // 非同期処理DとEを開始
    final String resultD = await target.functionD(resultA, resultB);
    final String resultE = await target.functionE(resultB, resultC);

    // Fの結果を待機
    final String finalResult = await target.functionF(resultD, resultE);

    // 最終結果を取得
    expect(finalResult, expectedResult);
  });

  test('中途半端な改善', () async {
    final target = SynchronousFunction();

    // 非同期処理A, B, Cを開始
    final Future<String> resultA = target.functionA();
    final Future<String> resultB = target.functionB();
    final Future<String> resultC = target.functionC();

    final resultABC = await Future.wait([resultA, resultB, resultC]);

    // 非同期処理DとEを開始
    final Future<String> resultD = target.functionD(resultABC[0], resultABC[1]);
    final Future<String> resultE = target.functionE(resultABC[1], resultABC[2]);

    final resultDE = await Future.wait([resultD, resultE]);

    // Fの結果を待機
    final String finalResult = await target.functionF(resultDE[0], resultDE[1]);

    // 最終結果を取得
    expect(finalResult, expectedResult);
  });

  test('完全な改善', () async {
    final target = FutureFunction();

    // 非同期処理A, B, Cを開始
    final Future<String> resultA = target.functionA();
    final Future<String> resultB = target.functionB();
    final Future<String> resultC = target.functionC();

    // 非同期処理DとEを開始
    final Future<String> resultD = target.functionD(resultA, resultB);
    final Future<String> resultE = target.functionE(resultB, resultC);

    // Fの結果を待機
    final Future<String> finalResult = target.functionF(resultD, resultE);

    // 最終結果を取得
    expect(await finalResult, expectedResult);
  });
}