初心者でも簡単!Dartで学ぶ並列処理の基本と10の応用サンプルコード

Dartで並列処理を学ぶ初心者向けのイラストDart
この記事は約20分で読めます。

 

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

基本的な知識があればカスタムコードを使って機能追加、目的を達成できるように作ってあります。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を凌駕する現役のプログラマチームによって監修されています。

サイト内のコードを共有する場合は、参照元として引用して下さいますと幸いです

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

今日は、Dart言語を使った並列処理の基本から応用までを学んでいきます。

DartはGoogleによって開発されたモダンなプログラミング言語で、特にWeb開発やモバイルアプリ開発で注目されています。

この記事を読めば、Dartの基本的な使い方から、より高度な並列処理の技術までを習得することができるでしょう。

並列処理は、複数の処理を同時に行うことで、アプリケーションのパフォーマンスを大幅に向上させることができます。

しかし、初心者にとっては少し難しい概念かもしれません。

この記事では、並列処理の基本から、具体的なサンプルコードを通して、その応用までを段階的に解説していきます。

これにより、読者の皆さんがDartでのプログラミングスキルを一層深めることができるはずです。

●Dartの基本概念

Dartは、主にクライアントサイドのWeb開発に使われるプログラミング言語です。

Googleによって開発され、特にFlutterフレームワークとの組み合わせでモバイルアプリ開発に広く利用されています。

Dartの特徴は、オブジェクト指向プログラミングをベースにしながらも、関数型プログラミングの要素を取り入れている点にあります。

○Dart言語の特徴

Dart言語の最大の特徴は、その柔軟性と生産性の高さです。

オブジェクト指向言語であるため、クラスやオブジェクトを使ったモジュール化が容易で、大規模なアプリケーション開発にも適しています。

また、DartはJIT(Just-In-Time)コンパイルとAOT(Ahead-Of-Time)コンパイルの両方に対応しており、開発時はリアルタイムでのコード変更が可能で、本番環境では最適化されたコードを実行できるという利点があります。

○並列処理とは何か

並列処理とは、複数の処理を同時に実行することを指します。

これにより、アプリケーションの効率を大幅に向上させることができます。

例えば、ユーザーの入力を待ちながら、バックグラウンドでデータの読み込みを行うような場合に有効です。

Dartでは、FutureやStreamといったクラスを使用して、この並列処理を実現します。

●Dartでの並列処理の基本

Dartでのプログラミングにおいて、並列処理は非常に重要な概念です。

並列処理により、プログラムは複数のタスクを同時に処理することが可能となり、全体のパフォーマンスと効率を向上させることができます。

ここでは、Dartでの並列処理の基本的な要素であるFutureとStreamについて解説します。

○Futureとは

FutureはDartで非同期処理を行うためのクラスです。

非同期処理とは、プログラムの主要な実行フローをブロックせずに、バックグラウンドでタスクを実行する方法を指します。

例えば、ファイルの読み込みやネットワークリクエストなど、時間がかかる操作を行う際に非同期処理が用いられます。

Futureを使用すると、非同期処理の完了時に結果を受け取ることができ、それまでの間に他の処理を続行することが可能です。

この特性は、ユーザーインターフェイスが反応しなくなるのを防ぐために特に重要です。

○Streamとは

StreamはDartで連続的なデータの流れを扱うためのクラスです。

Streamを利用することで、時間の経過と共に一連のデータを非同期に処理することが可能となります。

例えば、ユーザーの入力やファイルからのデータの読み込みなど、連続的に発生するイベントを扱う場合にStreamが用いられます。

Streamはデータが利用可能になるたびにそれを提供し、リスナー(Listener)を通じてデータを受け取ることができます。

これにより、プログラムはデータが到着するたびに必要な処理を行うことが可能となります。

●Dartにおける並列処理の実装方法

Dartでの並列処理を実装する方法は、主にFutureとStreamを使用します。

これらを使うことで、非同期処理や連続的なデータの取り扱いが可能となります。

ここでは、具体的な実装方法として、Futureを使った非同期処理とStreamを使ったデータの流れの管理について詳しく解説します。

○Futureを使った非同期処理

Futureを使用した非同期処理の一般的なパターンは、何らかの長時間実行される処理(例えば、ファイルの読み込みやネットワークリクエスト)を非同期で実行し、その結果を後から取得するというものです。

Future<String> fetchData() {
  // 非同期でデータを取得する処理をシミュレーション
  return Future.delayed(Duration(seconds: 4), () => 'データ取得完了');
}

void main() {
  print('データ取得を開始します');
  fetchData().then((data) {
    print(data); // データ取得完了
  });
  print('メイン処理を続行します');
}

このコードでは、fetchData 関数は非同期でデータを取得し、4秒後に ‘データ取得完了’ という文字列を返します。

main 関数では、fetchData が終了するのを待たずに、メイン処理を続行することができます。

○Streamを使ったデータの流れの管理

Streamを使用すると、時間の経過とともに発生するデータ(例えば、ユーザーからの入力やファイルからの逐次的な読み込み)を処理することができます。

ここでは、Streamを使用してデータを順次処理するサンプルコードを紹介します。

Stream<int> countStream(int max) async* {
  for (int i = 0; i < max; i++) {
    await Future.delayed(Duration(seconds: 1));
    yield i; // 毎秒カウントを一つずつ出力
  }
}

void main() {
  countStream(5).listen((count) {
    print('カウント: $count'); // カウント: 0, カウント: 1, ..., カウント: 4
  });
}

このコードでは、countStream 関数は最大値までカウントし、それをStreamとして出力します。

main 関数では、このStreamからのデータをlistenメソッドで受け取り、コンソールに出力します。

●Dartでの並列処理の応用例

Dartでの並列処理は多岐にわたるアプリケーションで活用することができます。

ここでは、いくつかの具体的な応用例とそれに伴うサンプルコードを紹介します。

これらの例を通じて、並列処理の幅広い可能性を理解し、自身のプロジェクトに応用することができるでしょう。

○サンプルコード1:非同期データ取得

Web APIからデータを非同期に取得する場合、Futureを使用して効率よく処理を行うことができます。

下記のコードは、非同期にデータを取得し、完了後に処理を行う一例です。

Future<String> fetchDataFromAPI() async {
  // ここでWeb APIからデータを非同期に取得
  await Future.delayed(Duration(seconds: 2)); // シミュレーションのための遅延
  return 'APIから取得したデータ';
}

void main() {
  fetchDataFromAPI().then((data) {
    print('取得したデータ: $data');
  });
  print('データ取得中...');
}

このコードでは、fetchDataFromAPI 関数が非同期でAPIからデータを取得し、そのデータを then メソッドで処理しています。

○サンプルコード2:ユーザー入力の非同期処理

ユーザーからの入力を非同期に処理する場合、Streamを用いることができます。

下記のコードは、ユーザーからの入力を非同期で受け取り、それを処理する方法を表しています。

import 'dart:io';

Stream<String> getUserInput() async* {
  while (true) {
    yield stdin.readLineSync() ?? ''; // ユーザー入力を取得
  }
}

void main() {
  getUserInput().listen((input) {
    print('ユーザー入力: $input');
  });
}

このコードでは、getUserInput 関数がユーザーからの入力を連続して受け取り、listen メソッドでそれぞれの入力を処理しています。

○サンプルコード3:ファイル読み込みの非同期処理

ファイルの読み込みは、特に大きなファイルの場合、時間がかかることがあります。

Dartでは、このような処理を非同期で実行することで、アプリケーションの反応性を維持することができます。

下記のサンプルコードでは、非同期にファイルを読み込む方法を表しています。

import 'dart:io';

Future<String> readFile(String filePath) async {
  return File(filePath).readAsString(); // ファイルを非同期に読み込む
}

void main() async {
  var data = await readFile('path/to/your/file.txt');
  print('ファイルの内容: $data');
}

このコードでは、readFile 関数が指定されたファイルパスから非同期にファイルを読み込み、その内容を返しています。

main 関数では、await を使用してファイルの読み込みが完了するまで待機し、完了後にファイルの内容を表示します。

○サンプルコード4:Web APIからのデータ取得

Web APIからデータを取得する際にも、非同期処理が役立ちます。

これにより、データの取得中に他の処理をブロックせずに実行することが可能です。

下記のサンプルコードは、Web APIから非同期にデータを取得する方法を表しています。

import 'dart:io';
import 'dart:convert';

Future<dynamic> fetchDataFromAPI(String url) async {
  var response = await HttpClient().getUrl(Uri.parse(url));
  var result = await response.close();
  return json.decode(await result.transform(utf8.decoder).join());
}

void main() async {
  var data = await fetchDataFromAPI('https://api.example.com/data');
  print('APIから取得したデータ: $data');
}

このコードでは、fetchDataFromAPI 関数が指定されたURLから非同期にデータを取得し、JSON形式で解析しています。

main 関数では、APIからのデータ取得を待機し、取得したデータを表示しています。

○サンプルコード5:並列データ処理

大量のデータを効率的に処理する場合、並列処理は非常に役立ちます。

下記のサンプルコードでは、複数のデータソースからのデータを並行して処理する方法を表しています。

Future<void> processParallelData(List<String> dataSource) async {
  await Future.wait(dataSource.map((data) async {
    // データ処理のロジック
    await Future.delayed(Duration(seconds: 1)); // 処理シミュレーション
    print('$data を処理しました');
  }));
}

void main() async {
  await processParallelData(['データ1', 'データ2', 'データ3']);
}

このコードでは、processParallelData 関数がリスト内の各データソースに対して並行して処理を行っています。

Future.wait はすべての非同期処理が完了するまで待機します。

○サンプルコード6:エラーハンドリングと再試行

非同期処理では、エラーを適切に処理し、必要に応じて再試行することが重要です。

下記のサンプルコードでは、非同期処理中に発生したエラーを捕捉し、再試行する方法を表しています。

Future<String> fetchDataWithRetry(String url, {int retries = 3}) async {
  int attempts = 0;
  while (attempts < retries) {
    try {
      // ここで非同期のデータ取得処理
      await Future.delayed(Duration(seconds: 1)); // 処理シミュレーション
      return 'データ取得成功';
    } catch (e) {
      attempts++;
      if (attempts == retries) rethrow;
    }
  }
  return '';
}

void main() async {
  try {
    var data = await fetchDataWithRetry('https://api.example.com/data');
    print(data);
  } catch (e) {
    print('データ取得に失敗しました: $e');
  }
}

このコードでは、fetchDataWithRetry 関数が指定されたURLからデータを取得しようと試み、エラーが発生した場合は指定された回数だけ再試行します。

エラーが継続した場合、最終的にエラーを投げます。

○サンプルコード7:タイマーを使った非同期処理

タイマーを使用した非同期処理は、特定の時間が経過した後に何らかのアクションを実行する場合に役立ちます。

下記のサンプルコードは、指定された時間後にメッセージを表示する単純なタイマーの実装を表しています。

Future<void> startTimer(int seconds) async {
  await Future.delayed(Duration(seconds: seconds));
  print('$seconds 秒が経過しました');
}

void main() async {
  await startTimer(3); // 3秒後にメッセージを表示
}

このコードでは、startTimer 関数が指定された秒数の遅延後にメッセージを表示します。

Future.delayed を用いることで、非同期的にタイマーを実装しています。

○サンプルコード8:カスタムイベントの非同期処理

カスタムイベントを非同期で処理することは、特定のアクションが発生した際に動的なレスポンスを生成するのに適しています。

下記のサンプルコードでは、カスタムイベントを非同期に処理する方法を表しています。

import 'dart:async';

// カスタムイベントを発生させる関数
StreamController<String> eventController = StreamController<String>();

void emitEvent(String event) {
  eventController.add(event);
}

Future<void> handleEvent() async {
  await for (var event in eventController.stream) {
    print('イベントを受信しました: $event');
  }
}

void main() async {
  handleEvent();
  emitEvent('イベント1');
  emitEvent('イベント2');
}

このコードでは、StreamController を使用してカスタムイベントを発行し、handleEvent 関数でこれらのイベントを非同期に受信して処理しています。

○サンプルコード9:Streamを使ったリアルタイムデータ処理

リアルタイムデータの処理は、特にストリーミングデータやライブデータのハンドリングにおいて重要です。

下記のサンプルコードでは、Streamを使用してリアルタイムデータを処理する方法を表しています。

import 'dart:async';

Stream<int> generateDataStream(int max) async* {
  for (int i = 0; i < max; i++) {
    await Future.delayed(Duration(seconds: 1));
    yield i;
  }
}

void main() {
  generateDataStream(5).listen((data) {
    print('リアルタイムデータ: $data');
  });
}

このコードでは、generateDataStream 関数がリアルタイムにデータを生成し、そのデータは listen メソッドで処理されています。

このようにStreamを使用することで、データが生成されるたびに即座に反応する処理を実装することが可能です。

○サンプルコード10:複数の非同期処理の同期

複数の非同期処理を効率的に管理し、すべての処理が完了したことを検知することは、多くのアプリケーションにおいて重要です。

下記のサンプルコードでは、複数の非同期処理を同期させる方法を表しています。

Future<String> asyncTask1() async {
  await Future.delayed(Duration(seconds: 2));
  return 'タスク1完了';
}

Future<String> asyncTask2() async {
  await Future.delayed(Duration(seconds: 3));
  return 'タスク2完了';
}

void main() async {
  var results = await Future.wait([asyncTask1(), asyncTask2()]);
  print(results); // ['タスク1完了', 'タスク2完了']
}

このコードでは、asyncTask1asyncTask2 という2つの非同期タスクを定義し、Future.wait を使用してこれらのタスクがすべて完了するのを待ちます。

これにより、複数の非同期処理の完了状況を一括で管理できます。

●並列処理の注意点と対処法

Dart言語での並列処理を効果的に行うためには、いくつかの重要な注意点があります。一つ目はリソース管理です。

Dartでは非同期処理を通じてリソースを効率的に使用できますが、これらのリソースの管理を誤るとパフォーマンスの低下やアプリケーションの不安定化を招きます。

リソースの過剰利用はメモリリークや処理の遅延を引き起こす可能性があるため、非同期処理の開始と終了を適切に管理することが重要です。

また、エラーハンドリングの重要性も無視できません。

非同期処理では、エラーが発生した場合に適切に対処することが必要です。

これは特にネットワーク通信やデータベース操作など、外部のシステムとのやり取りを伴う場合に顕著です。

例外が発生した際には、それを捕捉し、適切な処理を行う必要があります。これにはtry-catchブロックの使用や、FutureやStreamのエラーハンドリング機能の利用が含まれます。

○パフォーマンスの考慮事項

並列処理のパフォーマンスに関しては、複数の非同期タスクが互いに干渉しないようにすることが肝心です。

Dartの並列処理では、各タスクが独立して処理されるように設計されているため、タスク間でのリソース競合を避ける工夫が必要です。

例えば、大量のデータを扱う処理では、データを適切なサイズのチャンクに分割して処理することが効果的です。

また、CPU集中型のタスクは他のタスクの実行に影響を与えないよう、適切なタイミングで実行する必要があります。

○エラー処理とデバッグ

エラー処理においては、非同期処理のコンテキストで発生したエラーを適切にキャッチし、処理する必要があります。

Dartでは、Future.catchErrorやStream.handleErrorなどのメソッドを使用してエラーを捕捉できます。

これにより、エラー発生時に特定の処理を行ったり、エラーをログに記録したりすることが可能です。

●Dartの並列処理をカスタマイズする

Dartにおける並列処理のカスタマイズは、非同期処理を更に柔軟かつ効率的に扱うための重要な手法です。

特に、FutureやStreamをカスタマイズすることで、様々な非同期処理のシナリオに対応することが可能になります。

たとえば、特定の条件下でのみ非同期処理を実行したい場合や、非同期処理の結果に基づいて複雑なロジックを組み立てたい場合など、カスタマイズによってこれらの要件を満たすことができます。

カスタマイズの一つの方法としては、FutureやStreamのコンストラクタを使用して自分自身の非同期処理を作成することが挙げられます。

このアプローチにより、既存の非同期APIに依存せず、特定のビジネスロジックに合わせた非同期処理を実装することが可能です。

○カスタム非同期関数の作成

カスタム非同期関数の作成は、特定の非同期処理を自分のニーズに合わせて設計することを可能にします。

ここでは、独自の非同期処理を作成するサンプルコードを紹介します。

Future<String> fetchCustomData() async {
  // ここで非同期処理を行います。例えばAPIからのデータ取得など
  await Future.delayed(Duration(seconds: 2)); // 2秒のディレイを模擬
  return 'カスタムデータ';
}

void main() async {
  String data = await fetchCustomData();
  print(data); // カスタムデータが出力される
}

このコードでは、fetchCustomData関数が非同期処理をカプセル化しています。

この関数は2秒後に”カスタムデータ”という文字列を返すというシンプルな例ですが、実際には任意の非同期処理をこのパターンに従って実装することができます。

○既存の非同期処理のカスタマイズ

既存の非同期処理をカスタマイズする際には、既に存在するFutureやStreamを変換するか、新たな機能を追加することが一般的です。

たとえば、あるAPIからデータを取得するFutureがあるとします。

このFutureの結果を加工したり、特定の条件下でのみ処理を続けたりするために、次のようなコードが使用されることがあります。

Future<String> fetchApiData() async {
  // APIからデータを取得する想定の非同期処理
  await Future.delayed(Duration(seconds: 1)); // 1秒のディレイを模擬
  return 'APIデータ';
}

Future<String> processApiData() async {
  String apiData = await fetchApiData();
  return '加工済み: $apiData'; // 加工後のデータを返す
}

void main() async {
  String data = await processApiData();
  print(data); // 加工済み: APIデータ が出力される
}

この例では、fetchApiData関数がAPIからデータを取得し、processApiData関数がそのデータを加工しています。

このようにして、既存の非同期処理をカスタマイズし、より複雑な処理フローを構築することができます。

まとめ

この記事を通して、Dartでの並列処理の基本から応用に至るまでの幅広い概念と実践的な技術を紹介しました。

初心者でも理解しやすいように、Dartの並列処理に関する基本的な概念から始め、実際に使用されるFutureとStreamの基本、さらにはこれらを利用した非同期処理の実装方法について詳しく解説しました。

Dartにおける並列処理は、そのパワフルさと柔軟性から、多くの開発者にとって魅力的な選択肢です。

この記事が、Dartを用いた並列処理の理解を深め、より効率的で高品質なアプリケーション開発の助けとなることを願っています。