実践Flutter

Flutterでスマホを中心にアプリ制作していきます。

実践Dart:非同期プログラミング

はじめに

ここでは非同期プログラミングについてまとめていきます。

非同期の仕組みは、重たい処理をする必要があるときに画面が固まってしまうということを可能な限り避けるために主に用いられます。

とくにネットを介したデータのロードなどで時間がかかるときに、なんらか画面を動かして重たい処理を待つ仕組みを作らないと、たとえ数秒でも画面をフリーズさせたらユーザにアプリが落ちたと思われかねませんからね。

なので重たい処理をするときに、手番を握って画面を固めてしまうのではなく、GUI側に処理を回していく仕組みが必要になります。

このあたりの話に慣れていないかたは、とりあえずざっと見て雰囲気を確かめるだけでOKです。重たい処理は殆どの場合、サードパーティのライブラリのなかで発生します。そのライブラリの使い方には予めここで述べるFutureやasync/awaitをからめたコード例を示してくれています。なのでこれを理解できるようになっていれば、まずはOKです。




Future型

FutureはT型の値がすこし先の未来に得られることを意味する型です。

Flutterの初歩の段階でFuture型を直接自分で考えて使うことはあまりないですので、一旦ざっと眺めながらasync/awaitまでスキップしてもOKです。気楽に行きましょう。

下記例のなかで「heavyProcess()」は重たい処理を担う関数であると仮定します:

//重たい計算を含む関数をFutureでラップします。
Future<int> calc(){
  var ret = heavyProcess();
  
  //return retをこの形でラップします。
  //Future<T>.value()は名前付きコンストラクタの形ですね。
  return Future<int>.value(ret);
}

//DB処理やネット処理など重たい計算だとします。
int heavyProcess(){
  var ret = 0;
  for(int i=0; i<10000; i++){
    ret += i*i;
  }
  return ret;
}

void main(){
  Future<int> v = calc();

  //値vが到着したら、この処理を実行、というコールバック関数を予約しておきます。
  v.then((int n)=>print("done "+n.toString()));

  //こちらはすぐ終わる処理
  print("processing...");
}

これを実行すると、結果は:

processing...
done 333283335000

となります。後に記述されているprint文が先に実行されているのがわかります。

またthenにcatchError()をカスケードさせることで、例外処理を付加することができます。

  v.then((int n)=>print("done "+n.toString()))
    .catchError((e)=>print(e.stackTrace));

エラーを返す場合はFuture.error()という名前付きコンストラクタを使います。

Future<int> calc(){
  var ret = heavyProcess();
  
  //なんらかのエラーがあった場合  
  return Future<int>.error("oops");
}

void main(){
  Future<int> v = calc();

  //カスケードの中でcatchErrorを入れています
  v.then((int n)=>print("done "+n.toString()))
        .catchError((e)=>print(e.toString()));
  print("processing...");
}

こうするとエラーのスロー~キャッチをFutureの文脈で行うことができます。

またFuture.delayed()という名前付きコンストラクタもあります。これは与えられた時間を待機してから処理を実行する形になります。

Future<int> calc(){
  var ret = heavyProcess();
  
  //2秒待ってから実行
  return Future<int>.delayed(Duration(seconds: 2), ()=>ret);
}

void main(){
  Future<int> v = calc();
  v.then((int n)=>print("done "+n.toString()));
  print("processing..."); 
}

 

asyncとawait

asyncとawaitは前節のthen()の使用を避けるためのシンタックスシュガーです。

これは有用なライブラリを使う上で頻繁に用いますので、書き方と意味合いについては頭に留めておくとよいです。

Future<int> calc(){
  var ret = heavyProcess();
  
  return Future<int>.delayed(Duration(seconds: 2), ()=>ret);
}

//DB処理やネット処理など重たい計算だとします
int heavyProcess(){
  var ret = 0;
  for(int i=0; i<10000; i++){
    ret += i*i;
  }
  return ret;
}

//asyncキーワードをブロックの前に配置
void main() async {
  //Future型の関数の前にawaitをつける
  int v = await calc();
  
  print(v);
  print("processing...");
}

これでthenによるカスケード構文を避けることができます。

ただしasyncを付加したブロックのすべてがFutureの値が来るまで止まりますので:

333283335000
processing...

実行結果はこうなります。2秒ウエイトを掛けていますから、しっかり2秒止まってしまいます。

なのでasyncブロックはウエイトするもののみを含めます:

Future<int> calc(){
  var ret = heavyProcess();
  
  return Future<int>.delayed(Duration(seconds: 2), ()=>ret);
}

int heavyProcess(){
  var ret = 0;
  for(int i=0; i<10000; i++){
    ret += i*i;
  }
  return ret;
}

//ウエイトする処理のみをasyncで囲うようにする
void heavy() async{
  int v = await calc();
  print(v);
}

void main() {
  heavy();
  print("processing...");
}

また、Future.value()の名前付きコンストラクタでFuture型の値を返すのが本来の実装ですが、シンタックスシュガーとしてasyncをブロックの前につけて素の型を返すとFuture型に自動的にラップされます:

Future<int> calc() async{
  var ret = heavyProcess();
  
  return ret;  //Future<int>のコンストラクタにくるんでいない
}

 

ストリーム

ストリームはデータが流れるパイプのようなもので、そこにデータを生成する人が適宜データを流すと、これをチェックしているサブスクライバ(購読者)が興味のあるデータが来たことを知ることができる仕組みです。

ここもFlutter初歩としては、こういうものがあるのだ、という認識程度でまずはOKです。

登場する要素は以下です:

  • Generator :データを生成してストリームにデータを流す。
  • Stream :データを流す仕組み。
  • Subscriber :購読者。登録した型のデータがストリームに流されると通知を受ける。

Generatorには非同期型と同期型の二つのタイプがあります:

  • 非同期型 :Streamを返す。これにより非同期なデータフローを扱うことができる。
  • 同期型 :Iterableを返す。データは連続データとしてループのなかで扱う。
//Stream<T>型、ボディの前にasync* キーワード
Stream<int> numbers() async*{
  for(var i=0; i<5; i++){
    //1秒ウエイト
    await Future.delayed(Duration(seconds: 1));
    //Stream<T>型の関数ではreturnするのではなくyield文で値を生成
    //ストリームに流す
    yield i*i;
  }
}

//非同期を扱うのでasyncキーワードをつける
void sub() async{
  //Stream<T>型になる
  var st = numbers();  
  print("go for it");

  //await forでストリームの値を読み込む
  await for(var v in st){
    print(v);
  }
  print("done");
}

//エントリポイント
void main(){
  sub();
  print("next step");
}

実行結果は以下のようになります。1秒毎にウエイトがかかっていますので実行は徐々に進みますが、非同期ですので「go for it」のあと「next step」が表示され、そのあと秒ごとに次の行が出力されていきます:

go for it
next step
0
1
4
9
16
done






おわりに

Dartの非同期処理についてまとめてみました。まずはざっと眺めてOK、という箇所が多かったですが、一旦async / awaitの形をよく使うので意味を知っておくくらいで良いと思います。

すぐに必要にならない複雑なものは、実践で必要にかられて学ぶのが一番ですからね。

少し慣れてきてFlutter/Dartのライブラリの実装から色々と学ぼうとすると、そのレベルでは頻繁にでてきます

ちなみにDartの並列実行の仕組みは、ほかの言語でよく見るスレッド(thread)ではなく、アイソレート(isolate)という仕組みが基礎になっています。

アイソレートはスレッドと違って共有のメモリを互いに持ちません。なのでスレッドプログラミングで悩まされる実行のタイミングによる謎の挙動、データ競合がそもそも発生しません

昔と比べて圧倒的に計算パワーが向上していますので、メッセージングなど少し重たい処理を使ってでも全体の仕組みは単純に、謎のバグが起こりにくいようにするという設計になっていますね。その方が生産性はずっと高くてよいですよね。

f:id:linkedsort:20211107014515j:plain