😸

【Dart】非同期処理(Future, Stream)

2023/04/22に公開

Dartの非同期処理(Future, Stream)についてまとめていきます。

参考

同期処理と非同期処理

同期処理は、一連のタスクが順番に実行され、一つのタスクが完了するまで次のタスクが始まらない処理方法です。
一方で、非同期処理は、複数のタスクが同時に実行され、タスクの完了を待たずに次のタスクが開始される処理方法です。

正直、少しわかりにくいので、非同期処理はどのような場面で使うのかを紹介していきます。

非同期処理を使うシチュエーションは?

  • ネットワークリクエストをするとき
  • ファイル入出力時
  • 重い計算処理を行うとき
  • UIの更新が必要なとき

一つずつ見ていきます。

ネットワークリクエストをするとき

サーバーからデータを取得する際、リクエストの応答に時間がかかることがあります。このとき、非同期処理を使用することで、応答を待っている間に他のタスクを実行でき、アプリケーションのレスポンスを向上させることができます。

ファイル入出力時

ディスク上のファイルを読み書きする際、ファイルのサイズやディスクの速度によって処理に時間がかかることがあります。非同期処理を使用することで、ファイル操作が完了するまでの待ち時間に他の処理を実行できます。

重い計算処理を行うとき

計算に時間がかかるタスクを実行する場合、非同期処理を使用して他の処理がブロックされないようにすることができます。例えば、画像処理やデータ解析など、CPU集中的なタスクは、UIスレッドをブロックしないようにバックグラウンドで非同期的に実行することで、アプリケーションのレスポンスが向上します。

UIの更新が必要なとき

グラフィカルなユーザーインターフェース(UI)を持つアプリケーションでは、非同期処理を使ってUIの更新を行うことができます。例えば、アプリケーションで何らかの変更があったときに、画面上に表示されている情報を最新の状態に反映させるために行われます。これにより、アプリケーションのレスポンスが向上し、ユーザーエクスペリエンスが改善されます。

FutureとStream

非同期処理をサポートするクラスとして、FutureとStreamがあります。
Futureは、単数データの非同期処理を実装する際に使います。
一方で、Streamは、複数データの非同期処理を実装する際に使います。

FutureとStreamの違い

Futureは、一度その関数を実行すれば返り値は1つです。したがって、1つしか結果が出ないので、変数の変化を追うことはできません。一方で、Streamは、データの流れを作る(どんどん値を運ぶ)ため、変数の変化を追うことができます。

Future

https://api.dart.dev/stable/2.19.6/dart-async/Future-class.html

例えば、以下のようなtask1→task2→task3を順に実行していくプログラムがあります。

import 'package:flutter/material.dart';

void main() {
  task1();
  task2();
  task3();
}

void task1() => debugPrint('task1 complete');
void task2() => debugPrint('task2 complete');
void task3() => debugPrint('task3 complete');

実行結果は、以下のとおりです。

task1 complete
task2 complete
task3 complete

上記のプログラムに対して、非同期処理のクラスであるFutureを実装してみます。
これは、task2のみ2秒後に実行されるというプログラムです。

import 'dart:async';

import 'package:flutter/material.dart';

void main() {
  task1();
  task2();
  task3();
}

void task1() => debugPrint('task1 complete');

//2秒後にPrintされる
Future<void> task2() async {
  Future<String> future = Future.delayed(const Duration(seconds: 2), () => 'task2 complete');
  String value =  await future;
  debugPrint(value);
}

void task3() => debugPrint('task3 complete');

すると結果は、先ほどのプログラムではtask1→task2→task3と順番に出力されていましたが、今回はtask2が2秒後に実行されるように宣言を行ったので、以下のようになりました。

task1 complete
task3 complete
task2 complete

このように、Futureは単数データの非同期処理を実装する際に使用することができます。

Stream

続いては、Streamについて例を挙げながら説明します。

https://dart.dev/tutorials/language/streams

以下のプログラムでは、numberStream()関数を定義し、1秒ごとに整数を生成するStreamを作成しています。main()関数内で、numberStream()から返されたStreamをリスニングして、データが利用可能になるたびにコンソールに出力しています。
リスニングする際に使用されるlistenメソッドは、Streamから送られてくる引数を都度取得することができます。
また、yieldを使用すると、ジェネレータ関数は一度に1つの値を返し、次の値が要求されるまで実行を一時停止します。

import 'dart:async';

void main() {
  // numberStreamからデータを受け取るリスナーを登録
  Stream<int> stream = numberStream();
  stream.listen((data) {
    print('task$data');
  }, onError: (error) {
    print('Error: $error');
  }, onDone: () {
    print('All tasks completed');
  });
}

// 1秒ごとに整数を生成するStreamを作成する関数
Stream<int> numberStream() async* {
  int counter = 1;
  while (counter < 6) {
    await Future.delayed(const Duration(seconds: 1));
    yield counter++;
  }
}

非同期処理を使うリスク

非同期処理を適切に使用することで、アプリケーションのパフォーマンスやユーザーエクスペリエンスを向上させることができます。ただし、非同期処理を使いすぎると、コードの可読性や保守性が低下する場合があるため、適切なバランスが重要です。

まとめ

今回は、Dartの非同期処理であるFutureとStreamについてまとめました。
Futureは、単数データの非同期処理を実装する際に使います。一方で、Streamは、複数データの非同期処理を実装する際に使います。
また、Streamでは、listenメソッドを使用することで、連続的にデータを取得することができます。

非同期処理には、使用することのリスクもありますが、うまく使うことができればアプリケーションの幅がとても広がる機能だと思っています!

Discussion