Dart异步和isolate

1,389 阅读9分钟

大多数真正的Dart程序是并发的。

Web应用通常由至少两个并发部分组成: 在用户设备的Web浏览器中运行的客户端程序及在浏览器外部运行的服务器程序,它通常运行在互联网上的另一台计算机上。许多服务器端同时服务多个客户端。这些是典型的应用场景,但还有很多其他场景。

Dart并发基于isolate。依赖于future和stream。

异步

当我们在Dart中调用方法或函数时,调用者将等待,直到被调用者被求值并返回结果。当被调用者在执行时,调用者将被堵塞。函数调用类似于呼叫电话,其中呼叫着等待另一方应答。方法调用是同步通信的一种形式: 调用者和被调用者实时同步它们的活动。

相比之下,在异步通信中,调用者发起动作后,并不等待动作完成,而是立即继续执行。异步通信适用的任务包括如输入或输出,其中请求的动作可能比较慢,等待动作完成是不切实际的。

异步是Dart并发方案的基础。Dart支持单个和多个isolate中的异步。

跨isolate通信基于异步消息传递。在其基本形式中,消息传递是一种"触发就忘记"的活动。发送者不保证消息送达或者返回响应。这类似于用老式邮政发送信件。我们可以通过发送消息来发起某些动作,但我们不知道该动作是否会执行,也不知道它的执行是否成功。当然,我们也不知道动作成功执行的结果是什么,也不清楚动作失败时抛出了什么错误。这是不能令人满意的。应用程序需要能够监控和响应它们启动的异步活动。为了解决这个需求,我们可以使用future。

future

future是代表可能还没有发生的运算结果的对象。结果可能再未来的没某个时间是可知的,因此被称为"future"。future在异步运算中是有用的。一旦做出执行异步运算的请求,就可以以同步方式立即返回future,而实际运算可以稍后运行。future向请求异步操作的代码提供了一个句柄,即一个请求运算过程的引用。在请求的运算完成后,future可以用来访问结果。我们把这称为future已经完成或者已经解决。

使用future

furture有一个then()方法,接收一个名为onValue 的闭包作为参数。该闭包在future成功完成时被调用。闭包本身接收一个参数,类型为T,代表future运算结果的类型。因此。当future完成时,它所代表的馅将会传送给onValve。

future 同样可以表示失败的运算,也定义了方法catchErrort(), 并且接收一个闭包作为输入参数。如果future代表的运算抛出异常,则调用闭包,即onError.

生成 future

将future返回给其调用者的代码时必须执行几个操作。它必须安排-一个在 稍后执行的运算中,必须实例化一个future 实例,并将该实例与稍后执行的运算相关联,使得在稍后执行的运算完成时,future将被运算结果完成。我们必须注意捕获稍后执行的运算所抛出的任何异常,并使用适当的错误来完成future。

future的构造函数为我们处理所有这些底层细节。它的代码看起来大致如下:

Future (Function computation) {

    try

    _completeWi thValue (computation()); catch (e

    completewithError(e) ;

    );
}

类Timer定义在dat:async中,拥有一个静态方法run0, 用于调度代码的执行。future的构造函数接收一一个闭包computation,用于封装用户希望调度执行的运算过程。但是,实际被调度的任务是-一个被设计来捕获computation结果的闭包,它将使被构造的future实例 顺利完成,无论计算成功与否。计算的完成是由私有方法completeWithValue 来处理。我们 不希望把这些暴露给future 的使用者,使用者不需要也不应该能影响future实例返回给他们的结果。

最终结果就是Dart开发者只需简单编写如下代码:

new Future (myComputation) ;

其中myComputation定义了我们需要的运算过程。

stream

stream是一个长度不确定的值列表。它可能是无限的,或者最终可能结束,但关键是我们不知道stream 何时结束或者是否已经结束。

stream 的具体例子 可以是随时间而改变的鼠标位置,或者是所有素数的列表,又或者是通过网络发送给我们的实况视频。

我们可以订阅(或监听) stream, 那意味着为steam注册一个或多 个回调函数。当新数据追加到srem中时,这些函数将被调用。

将stream视为一个集合是很自然的。集合上的许多操作都适用于stream。我们可以在 stream. 上调用map0,从而得到一个经过转换计算的 stream。例如,我们可以使用map将一对坐标转换映射为图片的坐标,那样就可以构建个使图片随着鼠标在场景内移动的应用。 使用map0派生的stream与其原始stream是1:1的。某些时候,用多个元素替换stre ntream 的元素是很有帮助的。在特殊情况下,如果转换系数为0,那么我们可以过滤掉strean n的 元素。expand0方法 (在许多其他语言中被称为flatMapO)允许我们这样做。

过滤元素可以使用expand0完成,但使用where()将更加简单。

对集合的某些操作需要遍历整个集合。在sreaem中,我们不能即时处理这种情况, 为这些操作返回的是future 例如,假设我们有一个包含所有素数的stream primes,我们相要将它们累加:

Future<int> sisypheanSum = sum (primes);

sisypheanSum. then((s) > print(s));

当然,如果我们的素数stream真的直持续运行下去,那么这将是一个无法完成的任务如果我们的stream在某时刻关闭,那么我们将得到结果。

isolate

Dart支持actor风格的并发模型。运行中的Dart程序由一个或多 个actor 组成,它们被称为isolate。 isolate 是有着自己的内存和单线程控制的计算过程。术语islate“源于独立实 国的分离, 因为isolate之间的内存在逻辑上是分离的。 isolate中的代码是按顺序运行的,任何并发都是运行多个isolate的结果。因为Dart没有共享内存的并发,所以不需要锁而且没有发生竞争的可能性。 由于isolate没有共享内存,所以它们之间可以通信的唯J方式是通过消息传递。Dart中的消息传递总是异步的。 不像一些语言,isolate 没有阻塞接收机制,因此不会发生死锁。

Port

一个isolate有多个port。Port是Dart isolate间通信的底层基础。Port有两种: send port 和receive port。

receive port是一 个接收消息的steam: send port则允许发送消息给isolate, 确切地说,它允许将消息发送给receive pont。 send port可以被receive port生成,它将把所有消息发送给对应的receive port。

spawning

在isolate中启动另一个 isolate 被称为spawning.生成isolate时需要指定-个库,isolate会从该库的main()方法开始执行,这个库被称为isolate的根库。

类isolafe提供了两种用于生成isolate 的类方法: 第1种是spawnUri(),它基于给定库 的URI来产生个isolate; 第2种是spawn(),它根据当前isolate的根库生成一个isolae。 Dart程序的执行开始于主isolate,它由Dart运行时产生。为了创建新的isolate,主isolate 中运行的代码必须生成它们。当一个isolate产生另一个 isolate 时,它有机会传递- 些初始 参数,这些关键参数是一种初始消息。对消息的定义可归纳为:消息可能是null、 数字、 布尔值、字符串、send port、 消息列表或键值对都是消息的map。初始消息通常包括一个 send port,新产生的isolate (spawnee) 可以利用该send port将消息发送回生成它的isolale(它的spawner)

spamner如何产生-个send port并传递给它的spawne呢? spawer创建recive port rl, 以其中提取一个send port sl,然后产生一个新的 isolata,并把新的send port传入:

main(){//在主isolate中

ReceivePort r1 = new ReceivePort();

SendPort s1 = r1. sendPort;

Isolate. spawnUri (new Uri (path:'. /otherIsolate.dart'), [], s1);

}

然后,spawnee创建它自身的receive port r2,并从中提取send port s2,将其通过s1发 送给spawner.这种求偶配对式动作生成了一对 可以互相交流的isolate:

main (args, SendPort s1){量//在otherIsolate.dart中

ReceivePort r2= new ReceivePort () ;

SendPort s2 = r2. sendPort;

s1. send(s2) ;

刚才描述的过程有点乏味,虽然只是每个iolale 开头的三行常规代码。然而,ponr 制是非常灵活的,可以在其, 上构建各种更高级别的机制,我们很快将会看到。

安全

isolate是Dart安全的基础。内存的隔离防止一个 isolate 影响另-个isolate 的状态。

isolate带来的一个后 果是,可以通过反射来破坏库的隐私,并且反射在isolate范围内是不受限制的。而isolate之间的反射又是另一回事了。

future、isolate的使用场景

实际开发过程当中, 我们更关注何时使用future, 何时使用isolate。通过对比future、isolate的使用方式, 我们可以很清晰的看到, future比较轻便、简单,isolate比较重。大多数建议使用future。其实isolate也有使用场景的。

  • 代码不会被中断, 那么就直接使用正常的同步执行
  • 代码可以独立运行而不会影响应用程序的流畅性, 建议使用future
  • 繁重的处理可能要花一些时间才能完成, 而且会影响应用程序的流畅性, 建议使用isolate

实际开发过程当中, 建议尽可能多地使用future(直接或间接通过异步方法), 因为一旦eventloop有空闲期, 就会执行future。可能有些抽象, 下面给出衡量选择:

  • 一个任务要花费几毫秒, 建议使用future
  • 一个任务要花费几百毫秒, 建议使用isolate
  • 官方说法, 一个任务执行时间超过16毫秒, 就会影响到UI流畅性, 建议使用isolate了

下面列出isolate的使用场景:

  • JSON解析: 解码JSON,这是HttpRequest的结果,可能需要一些时间,可以使用封装好的 isolate 的 compute 顶层方法
  • 加解密: 加解密过程比较耗时
  • 图片处理: 比如裁剪图片比较耗时
  • 从网络中加载大图