(十)Dart Asynchrony(异步)、Isolates

  • Dart 有一些语言特性来支持 异步编程。 最常见的特性是 async 方法和 await 表达式。

  • Dart 库中有很多返回 Future 或者 Stream 对象的方法。 这些方法是 异步的, 这些函数在设置完基本的操作后就返回了, 而无需等待操作执行完成。 例如读取一个文件,在打开文件后就返回了。

  • Future 和 JavaScript 中的 Promise 类似,代表在将来某个时刻会返回一个 结果。Stream 是一种用来获取一些列数据的方式,例如 事件流。 Future, Stream, 以及其他异步操作的类在 dart:async 库中。

  • dart:async 库在 web app 和命令行 app 都可以使用。 只需要导入 dart:async 即可使用:

import 'dart:async';

一、Future

有两种方式可以使用 Future 对象中的 数据:

  • 使用 asyncawait(要使用 await,其方法必须带有 async 关键字)
  • 使用 Future API
  • 使用 await 的表达式比直接使用 Future api 的代码要更加容易理解。

在 Dart 库中随处可见 Future 对象,通常异步函数返回的对象就是一个 Future。 当一个 future 执行完后,他里面的值 就可以使用了。

checkVersion() async {
  var version = await lookUpVersion();
  if (version == expectedVersion) {
    // Do something.
  } else {
    // Do something else.
  }
}

可以使用 try, catch, 和 finally 来处理使用 await 的异常:

try {
  server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 4044);
} catch (e) {
  // 无法绑定到端口时作出反应...
}

1.1.Declaring async functions(声明异步方法)

async 方法: 是函数体被标记为 async 的方法。 虽然异步方法的执行可能需要一定时间,但是异步方法立刻返回 (在方法体还没执行之前就返回了)。

checkVersion() async {
  // ...
}

lookUpVersion() async => /* ... */;

在一个方法上添加 async 关键字,则这个方法返回值为 Future。 例如,下面是一个返回字符串 的同步方法:

String lookUpVersionSync() => '1.0.0';

如果使用 async 关键字,则该方法 返回一个 Future,并且 认为该函数是一个耗时的操作。

Future lookUpVersion() async => '1.0.0';

注意:方法的函数体并不需要使用 Future API。 Dart 会自动在需要的时候创建 Future 对象。

1.2.Using await expressions with Futures(使用 await 表达式)

await 表达式具有如下的形式:

await expression

在一个异步方法内可以使用多次 await 表达式。 例如,下面的示例使用了三次 await 表达式 来执行相关的功能:

var entrypoint = await findEntrypoint();
var exitCode = await runExecutable(entrypoint, args);
await flushThenExit(exitCode);

await expression 中, expression 的返回值通常是一个 Future; 如果返回的值不是 Future,则 Dart 会自动把该值放到 Future 中返回。 Future 对象代表返回一个对象的 promise 。 await expression 执行的结果为这个返回的对象。 await expression 会阻塞住,直到需要的对象返回为止。

如果 await 无法正常使用,请确保是在一个 async 方法中。 例如要在 main() 方法中使用 await, 则 main() 方法的函数体必须标记为 async:

main() async {
  checkVersion();
  print('In main: version is ${await lookUpVersion()}');
}

1.3.Future 的API

使用 Future 的 then() 函数来 执行三个异步方法, 每个方法执行完后才继续执行后一个方法。

runUsingFuture() {
  //...
  findEntrypoint().then((entrypoint) {
    return runExecutable(entrypoint, args);
  }).then(flushThenExit);
}

下面是使用 await 表达式实现的同样功能的代码, 看起来更像是同步代码,更加容易理解:

runUsingAsyncAwait() async {
  //...
  var entrypoint = await findEntrypoint();
  var exitCode = await runExecutable(entrypoint, args);
  await flushThenExit(exitCode);
}

可以使用 then() 来在 future 完成的时候执行其他代码。例如 HttpRequest.getString() 返回一个Future,由于 HTTP 请求是一个 耗时操作。使用then() 可以在 Future 完成的时候执行其他代码 来解析返回的数据:

HttpRequest.getString(url).then((String result) {
  print(result);
});
// Should handle errors here.

使用 catchError() 来处理 Future 对象可能抛出的 各种异常和错误:

HttpRequest.getString(url).then((String result) {
  print(result);
}).catchError((e) {
  // Handle or ignore the error.
});

then().catchError() 模式就是异步版本的 try-catch

重要: 确保是在 then() 返回的 Future 上调用 catchError(), 而不是在 原来的 Future 对象上调用。否则的话,catchError() 就只能处理原来 Future 对象抛出的异常而无法处理 then() 代码 里面的异常。

1.4.Chaining multiple asynchronous methods(链接多个异步方法)

then() 函数返回值为 Future,可以把多个异步调用给串联起来。 如果 then() 函数注册的回调函数也返回一个 Future,而 then() 返回一个同样的 Future。如果回调函数返回的是一个其他类型的值, 则 then() 会创建一个新的 Future 对象 并完成这个 future。

Future result = costlyQuery();

return result.then((value) => expensiveWork())
             .then((value) => lengthyComputation())
             .then((value) => print('done!'))
             .catchError((exception) => print('DOH!'));

上面的示例中,代码是按照如下顺序执行的:

  1. costlyQuery()
  2. expensiveWork()
  3. lengthyComputation()

1.5.Waiting for multiple futures(等待多个future)

有时候,你的算法要求调用很多异步方法,并且等待 所有方法完成后再继续执行。使用 Future.wait()这个静态函数来管理多个 Future 并等待所有 Future 执行完成。

Future deleteDone = deleteLotsOfFiles();
Future copyDone = copyLotsOfFiles();
Future checksumDone = checksumLotsOfOtherFiles();

Future.wait([deleteDone, copyDone, checksumDone])
    .then((List values) {
      print('Done with all the long steps');
    });

二、Stream

Stream 在 Dart API 中也经常出现,代表一些列数据。 例如, HTML 按钮点击事件就可以使用 stream 来表示。 还可以把读取文件内容当做一个 Stream。

从 Stream 中获取数据两种方式:

  • 使用 async 和一个 异步 for 循环 (await for)
  • 使用 Stream API

2.1.Using asynchronous for loops with Streams(在循环中使用异步)

异步 for 循环具有如下的形式:

await for (variable declaration in expression) {
  // Executes each time the stream emits a value.
}

上面 expression 返回的值必须是 Stream 类型的。 执行流程如下:

  • 等待直到 stream 返回一个数据
  • 使用 stream 返回的参数 执行 for 循环代码,
  • 重复执行 1 和 2 直到 stream 数据返回完毕。
  • 使用 break 或者 return 语句可以 停止接收 stream 的数据, 这样就跳出了 for 循环并且 从 stream 上取消注册了。

如果异步 for 循环不能正常工作, 确保是在一个 async 方法中使用。 例如,要想在 main() 方法中使用异步 for 循环,则需要把 main() 方法的函数体标记为 async:

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

2.2.Using an asynchronous for loop(for循环的异步使用)

有时候可以使用异步 for 循环(await for)来 替代 Stream API。

例如下面的示例中,使用 Stream 的 listen() 函数来订阅 一些文件,然后使用一个方法参数来 搜索每个文件和目录。

void main(List arguments) {
  ...
  FileSystemEntity.isDirectory(searchPath).then((isDir) {
    if (isDir) {
      final startingDir = new Directory(searchPath);
      startingDir
          .list(
              recursive: argResults[RECURSIVE],
              followLinks: argResults[FOLLOW_LINKS])
          .listen((entity) {
        if (entity is File) {
          searchFile(entity, searchTerms);
        }
      });
    } else {
      searchFile(new File(searchPath), searchTerms);
    }
  });
}

下面是使用 await 表达式和异步 for 循环 实现的等价的代码, 看起来更像是同步代码:

main(List arguments) async {
  ...
  if (await FileSystemEntity.isDirectory(searchPath)) {
    final startingDir = new Directory(searchPath);
    await for (var entity in startingDir.list(
        recursive: argResults[RECURSIVE],
        followLinks: argResults[FOLLOW_LINKS])) {
      if (entity is File) {
        searchFile(entity, searchTerms);
      }
    }
  } else {
    searchFile(new File(searchPath), searchTerms);
  }
}

注意: 在使用 await for 之前请确保使用之后的代码看起来确实是 更加清晰易懂了。例如,对于 DOM时间监听器通常 不会使用 await for,原因在于 DOM 发送 无尽的事件流。 如果使用 await for 来在同一行注册两个 DOM 事件监听器, 则第二个事件在不会被处理。

2.3.Listening for stream data(监听stream数据)

要想在每个数据到达的时候就去处理,则可以选择使用 await for 或者 使用 listen() 函数来订阅事件:

// Find a button by ID and add an event handler.
querySelector('#submitInfo').onClick.listen((e) {
  // When the button is clicked, it runs this code.
  submitData();
});

上面的示例中, onClick 属性是 “submitInfo” 按钮提供的一个 Stream 对象。

如果你只关心里面其中一个事件,则可以使用这些属性: first, last, 或者 single。要想在处理事件之前先测试是否满足条件,则 使用 firstWhere(), lastWhere(), 或者 singleWhere() 函数。

如果你关心一部分事件,则可以使用 skip(), skipWhile(), take(), takeWhile(), 和 where() 这些函数。

2.4.Transforming stream data(转换stream数据)

经常你需要先转换 stream 里面的数据才能使用。 使用 transform() 函数可以生产另外一个数据类型 的 Stream 对象:

var stream = inputStream
    .transform(UTF8.decoder)
    .transform(new LineSplitter());

上面的代码使用两种转换器(transformer)。第一个使用 UTF8.decoder 来把整数类型的数据流转换为字符串类型的数据流。然后使用 LineSplitter 把字符串类型数据流转换为按行分割的数据流。 这些转换器都来至于 dart:convert 库。 参考 dart:convert ) 了解详情。

2.5.Handling errors and completion(处理错误和完善)

使用异步 for 循环 (await for) 和使用 Stream API 的 异常处理情况是有 区别的。

如果是异步 for 循环,则可以 使用 try-catch 来处理异常。

在 stream 关闭后执行的代码位于异步 for 循环 之后。

readFileAwaitFor() async {
  var config = new File('config.txt');
  Stream> inputStream = config.openRead();

  var lines = inputStream
      .transform(UTF8.decoder)
      .transform(new LineSplitter());
  try {
    await for (var line in lines) {
      print('Got ${line.length} characters from stream');
    }
    print('file is now closed');
  } catch (e) {
    print(e);
  }
}

如果你使用 Stream API,则需要 使用 onError 函数来处理异常。 stream 完成后执行的代码要通过 onDone 函数 来执行。

var config = new File('config.txt');
Stream> inputStream = config.openRead();

inputStream
    .transform(UTF8.decoder)
    .transform(new LineSplitter())
    .listen((String line) {
      print('Got ${line.length} characters from stream');
    }, onDone: () {
      print('file is now closed');
    }, onError: (e) {
      print(e);
    });

2.6.More information(更多信息)

关于在命令行应用中使用 Future 和 Stream 的更多示例,请参考 dart:io 里面的内容。 下面也是一些可以参考的文章和教程:

  • Asynchronous Programming: Futures

  • Futures and Error Handling

  • The Event Loop and Dart

  • Asynchronous Programming: Streams

  • Creating Streams in Dart

三、Isolates

现代的浏览器以及移动浏览器都运行在多核 CPU 系统上。 要充分利用这些 CPU,开发者一般使用共享内存数据来保证多线程的正确执行。然而, 多线程共享数据通常会导致很多潜在的问题,并导致代码运行出错。

所有的 Dart 代码在 isolates 中运行而不是线程。 每个 isolate 都有自己的堆内存,并且确保每个 isolate 的状态都不能被其他 isolate 访问。

待续。。。

你可能感兴趣的:((十)Dart Asynchrony(异步)、Isolates)