Dart学习笔记(29):异步编程

发表于2018-07-04 13:22 阅读(70)

目录
1 Async和await
2 Async*,sync*,and all the rest
….2.1 同步生成器:sync*
….2.2 异步生成器:async*
….2.3 yield*
3 Future API

本文地址:http://www.cndartlang.com/720.html

刚开始折腾Dart的时候,写了一篇关于异步与并发实例的帖子,但相关的知识点只是简单点了一下,实在是有些简陋,这里再提一下异步,作为之前内容的补充。

Dart是一个单线程编程语言,如果进行I/O操作或等待一个耗时的操作时,程序会阻塞。异步编程让程序在运行的时候不会被阻塞卡死,而在Dart中则使用Future对象来实现异步操作。

下面是一段同步代码,表示读取每日新闻摘要、彩票中奖号码、天气预报、棒球比分:

printDailyNewsDigest() {
  String news = gatherNewsReports(); // 需要花费一点时间
  print(news);
}

printWinningLotteryNumbers() {
  print('Winning lotto numbers: [23, 63, 87, 26, 2]');
}

printWeatherForecast() {
  print('Tomorrow\'s forecast: 70F, sunny.');
}

printBaseballScore() {
  print('Baseball score: Red Sox 10, Yankees 0');
}

main() {
  printDailyNewsDigest();
  printWinningLotteryNumbers();
  printWeatherForecast();
  printBaseballScore();
}

当收集新闻报道的时候,gatherNewsReports会使程序阻塞,无论花多长时间,必须取得返回值后才能继续运行,这使得用户被动等待,非常不友好,这时候就需要用到Future。

Future表示在将来的某个时候获取到一个值,当返回值为Future类型的函数被调用时,Dart会做两件事:
1、将要做的计算工作添加到函数队列中,并返回一个未完成的Future对象。
2、接下来,如果计算的值有效(包括异常),Future以该值完成计算。

要获取Future完成计算的值,可以使用下面2中方法:
1、使用async和await关键字
2、使用Future API

1、Async和await

从Dart 1.9开始,Dart添加了async、await关键字实现异步的功能。async和await关键字使得异步代码的编写非常简单,就和同步代码一样,并不使用Future API。如:

String getVersion() => '1.0.0';

改写为异步代码,需在大括号或 => 前添加async关键字。async关键字并不属于识别标志的一部分,它只是一个实现细节。从调用者的角度,调用async函数和传统函数并没有区别。对于函数return的类型并没有产生影响,最终都将封装为Future。同样,如果函数中throw异常,也会封装成Error Future。Dart中可以忽略函数返回值类型,但这并不推荐:

Future<String> getVersion() async => '1.0.0';

如果函数中,return的类型为T,那么函数的返回类型应该为Future<T>,或T的父类,否则会产生静态警告。如果函数中return的类型是Future<T>,那么函数的返回类型同样为Future<T>,而不是Future<Future<T>>。

之前的同步代码,输出函数可以如下进行改写:

Future printDailyNewsDigest() async {
  String news = await gatherNewsReports();
  print(news);
}

Future gatherNewsReports() async {
  String path =
      'https://www...';
  return (await HttpRequest.getString(path));
}

在main函数中,printDailyNewsDigest是第一个调用的函数,但由于异步运行,信息可能最后打印,下面是函数运行的顺序:

需注意的是,如果async函数没有return一个值,那么Dart将自动封装一个null值的Future。同时,await表达式可以使用多次,但只能在async标记的函数中使用。await声明的函数会进入阻塞状态,一直等待到完成计算,返回对应的值。使用await声明的表达式,等同于同步代码,可以用try-catch-finally捕获异常。

在await表达式中,表达式的值通常是Future,如果不是Future对象,则将该值自动封装为Future并添加到事件循环中,然后等待Future完成,如:await 1+1; 该特性使await表达式的行为拥有更好的可预见性,这也是Dart和其他语言类似功能的不同点之一。例如:如果一个循环中有一个无条件的await表达式,该特性可以确保表达式在每次循环的时候都被挂起。

在Stream数据流中,异步模式下,通常在then函数中对接收到的数据进行处理,如果要改为同步代码,除了调用同步模式的函数API外,还可以使用await for关键字,对数据进行遍历,如:

await for (var request in requestServer) {
  handleRequest(request);
}

2、Async*, sync*, and all the rest

前一节讨论了async函数和await表达式,这些功能是Dart中,初步支持异步编程和生成器的一部分。

在Dart 1.9中引入了函数生成器的概念,利用惰性函数计算结果序列,以提升性能。生成器有两种类型:同步生成器和异步生成器。同步生成器在需要的时候才生成值,然后使用者从生成器中拉取。异步生成器会以它自身的速度生成值,然后推送到使用者可以使用的地方。

为什么要支持生成器呢?有人可以自己实现,但那无疑是复杂而且冗长乏味的。Dart中内置的生成器使一切变得相当简单,而不用考虑去自定义可迭代类,实现moveNext()、current等成员函数、功能。

2.1 同步生成器:sync*

通过在函数主体前添加sync*可以快速标记该函数为同步生成器synchronous generator,解除很多手动定义可迭代类时复杂的公式化代码。假设我们要获取第一个自然数到n:

Iterable naturalsTo(n) sync* {
  print("Begin");

  int k = 0;
  while (k < n) yield k++;

  print("End");
}

main() {
  var it = naturalsTo(3).iterator;
  while(it.moveNext()) {
    print(it.current);
  }
}

当调用naturalsTo的时候,会立即返回Iterable(很像async函数立即返回Future),并且可以通过Iterable提取iterator。在有代码调用moveNext前,函数主体并不会执行。yield(生成)用于声明一个求值表达式,当第一次运行函数的时候,代码会执行到yield关键字声明的位置,并暂停执行,moveNext会返回true给调用者。函数会在下次调用moveNext的时候恢复执行。

运行结果:

Begin
0
1
2
End

当循环结束的时候,函数会隐式地执行return,这会使迭代终止,moveNext返回false。当然,作为一个专业的、实现了完整Iterable类API的迭代器和可迭代类来说,使用普通迭代器,这是非常冗长乏味的。

sync 和 * 可以分开,他们是不同的标记。如果你之前的代码使用了sync关键字,升级Dart并不会产生兼容性问题,sync并不是真正的预留字,并同样适用于async、await和yield。这些关键字仅在async函数和generator生成器函数中作为预留字(即:函数用async、sync*、async*标记)。

2.2 异步生成器:async*

异步生成器使用数据流来异步生成序列,通过在函数体前添加async*标识符来进行标记。这里同样用生成自然数来举例:

import 'dart:async';

Stream asynchronousNaturalsTo(n) async* {
  print("Begin");

  int k = 0;
  while (k < n) yield k++;

  print("End");
}

main() {
  asynchronousNaturalsTo(3).listen((v) {
    print(v);
  });
}

运行结果:

Begin
0
1
2
End

当运行asynchronousNaturalsTo的时候,会立即返回Stream,但函数体并不会执行,就像sync*生成器和async函数一样。一旦开始listen监听数据流,函数体会开始执行。当执行到yield关键字的时候,会将yield声明的求值表达式的计算结果添加到Stream数据流中。异步生成器没有必要暂停,因为数据流可以通过StreamSubscription进行控制。需注意的是,数据流不能重复监听。

import 'dart:async';

Stream get asynchronousNaturals async* {
  print("Begin");

  int k = 0;
  while (k < 3) {
    print("Before Yield");
    yield k++;
  }

  print("End");
}

main() {
  StreamSubscription subscription = asynchronousNaturals.listen(null);
  subscription.onData((value) {
    print(value);
    subscription.pause();
  });
}

运行结果:

Begin
Before Yield
0
Before Yield

上面的例子中,asynchronousNaturals用于获取小于3的自然数。async*等标识符同样适用于get函数。同时,与async*函数相关联的Stream可能会被pause暂停或cancel取消。如果被取消,控制权会转让给最近括起来从句末尾,即函数执行完毕。如果被暂停,函数会一直执行到yield声明的位置,然后暂停,直到Stream恢复。

2.3 yield*

之前的代码中,yield使虽然用起来确实有吸引力,但在写递归函数的时候,也可能遇到一些问题。下面是从大到小获取自然数的例子:

import 'dart:async';

Iterable naturalsDownFrom(n) sync* {
  if (n > 0) {
    yield n;
    for (int i in naturalsDownFrom(n-1)) { yield i; }
  }
}

main() {
  print(naturalsDownFrom(3));
}

运行结果:

(3, 2, 1)

由于每次调用sync*函数都会构建一个新的序列,因此在仅使用yield的情况下,只能遍历新构建的序列,通过yield将元素插入到当前序列中:

for (int i in naturalsDownFrom(n-1)) { yield i; }

上面的代码在功能上没什么问题,但我们来分析一下过程:
n=3时,只执行了一次yield n;;
n=2时,yield n;和yield i;分别执行了一次,并且执行的yield i;是上一层n=3时候的代码;
n=1时,执行了一次yield n;,两次yield i;,执行的yield i;是n=2和n=3时候的代码。

在代码中添加提示信息可以看得更明白:

int level = 0;

Iterable naturalsDownFrom(n) sync* {
  level++;

  if (n > 0) {
    print("level: $level n:$n");
    yield n;

    for (int i in naturalsDownFrom(n-1)) {
      print("level: $level i:$i");
      yield i;
    }
  }
}

运行结果:

level: 1 n:3
level: 2 n:2
level: 2 i:2
level: 3 n:1
level: 3 i:1
level: 3 i:1
(3, 2, 1)

为避免和变量名混淆,用x表示自然数的位置(即第x个自然数),y表示yield i;执行的次数,即x=1时,y=0;x=2时,y=1;x=3时,y=2。y的值为0,1,2……x-1。

总的来说,因为递归将元素插入序列的原因,共执行了x(x-1)/2次yield i;,时间复杂度为O(n^2),很明显地存在性能问题,而yield*则是为了解决此问题而设计的。yield*后面必须跟其他的(子)序列,并且会将后面(子)序列的所有元素插入到当前正在创建的序列中。

于是,代码可以修改如下:

Iterable naturalsDownFrom(n) sync* {
  if ( n > 0) {
    yield n;
    yield* naturalsDownFrom(n-1);
  }
}

在sync*函数中,子序列必须是一个Iterable可迭代对象;在async*函数中,子序列必须是一个Stream数据流。子序列可以为空,当为空的时候,yield*会跳过该表达式,并且不会暂停。

3、Future API

Future表示一个延迟的计算过程、任务,但并不立即执行。Future可以注册回调函数来处理计算的返回值或异常。API中关于Future的用法很多,通常使用构造函数来封装同步代码,如:

Future myFunc() {
  return new Future(() {
    //Do something
    return result;
  });
}

API中,Future的构造函数很丰富,如:

Future(dynamic computation())
Future.delayed(Duration duration, [dynamic computation()])
Future.sync(dynamic computation())
Future.value([value])

在网上的教程中,也有用Compeleter来实现异步函数的:

Future<String> myFunc() {
  Completer<String> comp = new Completer<String>();
  //Do something
  comp.complete("...");

  return comp.future;
}

但是并不推荐,通常Completer用于自定义Class中。如果你想从零开始创建一个Future,从基于回调函数的API转化成基于Future,可以如下使用Completer:

class AsyncOperation {
  Completer _completer = new Completer();

  Future<T> doOperation() {
    _startOperation();
    return _completer.future; // Send future object back to client.
  }

  // Something calls this when the value is ready.
  void _finishOperation(T result) {
    _completer.complete(result);
  }

  // If something goes wrong, call this.
  void _errorHappened(error) {
    _completer.completeError(error);
  }}

在使用Future API编写异步代码的时候,then函数用于注册Future完成计算时调用的回调函数,当Future完成计算时触发。如果有expensiveA、expensiveB、expensiveC三个异步函数,要实现三个函数依次运行,A执行完后运行B,B执行完后运行C,由于then函数返回值为Future,因此可以多次调用then形成Future链:

expensiveA()
  .then((_) => expensiveB())
  .then((_) => expensiveC());

注意:在then注册的回调函数的函数中,return的值会被封装为Future返回,如果没有return,会默认返回一个空值的Future。

上面的代码中,参数使用了下划线 _ ,其他语言可能表示占位符,但是在Dart中理解为占位符并不准确。应该说是习惯将 _ 视作占位符,Dart只是简单的将它视作一个变量,并可以在函数中使用:

print(_.runtimeType);

如果要调用多个异步函数,一并返回完成值,可以使用Future.wait,当所有Future完成后,返回List值列表:

Future.wait([expensiveA(), expensiveB(), expensiveC()])
      .then((List responses) => chooseBestResponse(responses));