Dart 异步编程详解

Dart 异步编程

编程中的代码执行,一般分为同步异步两种。简单说,同步就是按照代码的编写顺序,从上到下依次执行,这也是最简单的咱们最常接触的一种形式。可是同步代码的缺点也显而易见,若是其中某一行或几行代码很是耗时,那么就会阻塞,使得后面的代码不能被马上执行。html

异步的出现正是为了解决这种问题,它可使某部分耗时代码不在当前这条执行线路上马上执行,那究竟怎么执行呢?最多见的一种方案是使用多线程,也就至关于开辟另外一条执行线,而后让耗时代码在另外一条执行线上运行,这样两条执行线并列,耗时代码天然也就不能阻塞主执行线上的代码了。web

多线程虽然好用,可是在大量并发时,仍然存在两个较大的缺陷,一个是开辟线程比较耗费资源,线程开多了机器吃不消,另外一个则是线程的锁问题,多个线程操做共享内存时须要加锁,复杂状况下的锁竞争不只会下降性能,还可能形成死锁。所以又出现了基于事件的异步模型。简单说就是在某个单线程中存在一个事件循环和一个事件队列,事件循环不断的从事件队列中取出事件来执行,这里的事件就比如是一段代码,每当遇到耗时的事件时,事件循环不会停下来等待结果,它会跳过耗时事件,继续执行其后的事件。当不耗时的事件都完成了,再来查看耗时事件的结果。所以,耗时事件不会阻塞整个事件循环,这让它后面的事件也会有机会获得执行。编程

咱们很容易发现,这种基于事件的异步模型,只适合I/O密集型的耗时操做,由于I/O耗时操做,每每是把时间浪费在等待对方传送数据或者返回结果,所以这种异步模型每每用于网络服务器并发。若是是计算密集型的操做,则应当尽量利用处理器的多核,实现并行计算。api

在这里插入图片描述

Dart 的事件循环

Dart 是事件驱动的体系结构,该结构基于具备单个事件循环和两个队列的单线程执行模型。 Dart虽然提供调用堆栈。 可是它使用事件在生产者和消费者之间传输上下文。 事件循环由单个线程支持,所以根本不须要同步和锁定。服务器

Dart 的两个队列分别是网络

  • MicroTask queue 微任务队列多线程

  • Event queue 事件队列并发

在这里插入图片描述

Dart事件循环执行如上图所示异步

  1. 先查看MicroTask队列是否为空,不是则先执行MicroTask队列
  2. 一个MicroTask执行完后,检查有没有下一个MicroTask,直到MicroTask队列为空,才去执行Event队列
  3. Evnet 队列取出一个事件处理完后,再次返回第一步,去检查MicroTask队列是否为空

咱们能够看出,将任务加入到MicroTask中能够被尽快执行,但也须要注意,当事件循环在处理MicroTask队列时,Event队列会被卡住,应用程序没法处理鼠标单击、I/O消息等等事件。async

调度任务

注意,如下调用的方法,都定义在dart:async库中。

将任务添加到MicroTask队列有两种方法

import  'dart:async';

void  myTask(){
    print("this is my task");
}

void  main() {
    // 1. 使用 scheduleMicrotask 方法添加
    scheduleMicrotask(myTask);

    // 2. 使用Future对象添加
    new  Future.microtask(myTask);
}

将任务添加到Event队列

import  'dart:async';

void  myTask(){
    print("this is my task");
}

void  main() {
    new  Future(myTask);
}

如今学会了调度任务,赶忙编写代码验证以上的结论

import  'dart:async';

void  main() {
    print("main start");

    new  Future((){
        print("this is my task");
    });

    new  Future.microtask((){
        print("this is microtask");
    });

    print("main stop");
}

运行结果:

main start
main stop
this is microtask
this is my task

能够看到,代码的运行顺序并非按照咱们的编写顺序来的,将任务添加到队列并不等于马上执行,它们是异步执行的,当前main方法中的代码执行完以后,才会去执行队列中的任务,且MicroTask队列运行在Event队列以前。

延时任务

如须要将任务延伸执行,则可以使用Future.delayed方法

new  Future.delayed(new  Duration(seconds:1),(){
    print('task delayed');
});

表示在延迟时间到了以后将任务加入到Event队列。须要注意的是,这并非准确的,万一前面有很耗时的任务,那么你的延迟任务不必定能准时运行。

import  'dart:async';
import  'dart:io';

void  main() {
    print("main start");

    new Future.delayed(new  Duration(seconds:1),(){
        print('task delayed');
    });

    new Future((){
        // 模拟耗时5秒
        sleep(Duration(seconds:5));
        print("5s task");
    });

    print("main stop");
}

运行结果:

main start
main stop
5s task
task delayed

从结果能够看出,delayed方法调用在前面,可是它显然并未直接将任务加入Event队列,而是须要等待1秒以后才会去将任务加入,但在这1秒之间,后面的new Future代码直接将一个耗时任务加入到了Event队列,这就直接致使写在前面的delayed任务在1秒后只能被加入到耗时任务以后,只有当前面耗时任务完成后,它才有机会获得执行。这种机制使得延迟任务变得不太可靠,你没法肯定延迟任务到底在延迟多久以后被执行。

Future 详解

Future类是对将来结果的一个代理,它返回的并非被调用的任务的返回值。

void  myTask(){
    print("this is my task");
}

void  main() {
    Future fut = new  Future(myTask);
}

如上代码,Future类实例fut并非函数myTask的返回值,它只是代理了myTask函数,封装了该任务的执行状态。

建立 Future

Future的几种建立方法

  • Future()
  • Future.microtask()
  • Future.sync()
  • Future.value()
  • Future.delayed()
  • Future.error()

其中sync是同步方法,任务会被当即执行

import  'dart:async';

void  main() {
    print("main start");

new  Future.sync((){
    print("sync task");
});

new  Future((){
    print("async task");
});

    print("main stop");
}

运行结果:

main start
sync task
main stop
async task

注册回调

Future中的任务完成后,咱们每每须要一个回调,这个回调当即执行,不会被添加到事件队列。

import 'dart:async';

void main() {
  print("main start");


  Future fut =new Future.value(18);
  // 使用then注册回调
  fut.then((res){
    print(res);
  });

 // 链式调用,能够跟多个then,注册多个回调
  new Future((){
    print("async task");
  }).then((res){
    print("async task complete");
  }).then((res){
    print("async task after");
  });

  print("main stop");
}

运行结果:

main start
main stop
18
async task
async task complete
async task after

除了then方法,还可使用catchError来处理异常,以下

new Future((){
    print("async task");
  }).then((res){
    print("async task complete");
  }).catchError((e){
    print(e);
  });

还可使用静态方法wait 等待多个任务所有完成后回调。

import 'dart:async';

void main() {
  print("main start");

  Future task1 = new Future((){
    print("task 1");
    return 1;
  });

  Future task2 = new Future((){
    print("task 2");
    return 2;
  });
    
  Future task3 = new Future((){
    print("task 3");
    return 3;
  });

  Future fut = Future.wait([task1, task2, task3]);
  fut.then((responses){
    print(responses);
  });

  print("main stop");
}

运行结果:

main start
main stop
task 1
task 2
task 3
[1, 2, 3]

如上,wait返回一个新的Future,当添加的全部Future完成时,在新的Future注册的回调将被执行。

async 和 await

在Dart1.9中加入了asyncawait关键字,有了这两个关键字,咱们能够更简洁的编写异步代码,而不须要调用Future相关的API

async 关键字做为方法声明的后缀时,具备以下意义

  • 被修饰的方法会将一个 Future 对象做为返回值
  • 该方法会同步执行其中的方法的代码直到第一个 await 关键字,而后它暂停该方法其余部分的执行;
  • 一旦由 await 关键字引用的 Future 任务执行完成,await的下一行代码将当即执行。
// 导入io库,调用sleep函数
import 'dart:io';

// 模拟耗时操做,调用sleep函数睡眠2秒
doTask() async{
  await sleep(const Duration(seconds:2));
  return "Ok";
}

// 定义一个函数用于包装
test() async {
  var r = await doTask();
  print(r);
}

void main(){
  print("main start");
  test();
  print("main end");
}

运行结果:

main start
main end
Ok

须要注意,async 不是并行执行,它是遵循Dart 事件循环规则来执行的,它仅仅是一个语法糖,简化Future API的使用。

Isolate

前面已经说过,将很是耗时的任务添加到事件队列后,仍然会拖慢整个事件循环的处理,甚至是阻塞。可见基于事件循环的异步模型仍然是有很大缺点的,这时候咱们就须要Isolate,这个单词的中文意思是隔离。

简单说,能够把它理解为Dart中的线程。但它又不一样于线程,更恰当的说应该是微线程,或者说是协程。它与线程最大的区别就是不能共享内存,所以也不存在锁竞争问题,两个Isolate彻底是两条独立的执行线,且每一个Isolate都有本身的事件循环,它们之间只能经过发送消息通讯,因此它的资源开销低于线程。

从主Isolate建立一个新的Isolate有两种方法

spawnUri

static Future<Isolate> spawnUri()

spawnUri方法有三个必须的参数,第一个是Uri,指定一个新Isolate代码文件的路径,第二个是参数列表,类型是List<String>,第三个是动态消息。须要注意,用于运行新Isolate的代码文件中,必须包含一个main函数,它是新Isolate的入口方法,该main函数中的args参数列表,正对应spawnUri中的第二个参数。如不须要向新Isolate中传参数,该参数可传空List

Isolate中的代码:

import 'dart:isolate'; 


void main() {
  print("main isolate start");
  create_isolate();
  print("main isolate stop");
}

// 建立一个新的 isolate
create_isolate() async{
  ReceivePort rp = new ReceivePort();
  SendPort port1 = rp.sendPort;

  Isolate newIsolate = await Isolate.spawnUri(new Uri(path: "./other_task.dart"), ["hello, isolate", "this is args"], port1);

  SendPort port2;
  rp.listen((message){
    print("main isolate message: $message");
    if (message[0] == 0){
      port2 = message[1];
    }else{
      port2?.send([1,"这条信息是 main isolate 发送的"]);
    }
  });

  // 能够在适当的时候,调用如下方法杀死建立的 isolate
  // newIsolate.kill(priority: Isolate.immediate);
}

建立other_task.dart文件,编写新Isolate的代码

import 'dart:isolate';
import  'dart:io';


void main(args, SendPort port1) {
  print("isolate_1 start");
  print("isolate_1 args: $args");

  ReceivePort receivePort = new ReceivePort();
  SendPort port2 = receivePort.sendPort;

  receivePort.listen((message){
    print("isolate_1 message: $message");
  });

  // 将当前 isolate 中建立的SendPort发送到主 isolate中用于通讯
  port1.send([0, port2]);
  // 模拟耗时5秒
  sleep(Duration(seconds:5));
  port1.send([1, "isolate_1 任务完成"]);

  print("isolate_1 stop");
}

运行主Isolate的结果:

main isolate start
main isolate stop
isolate_1 start
isolate_1 args: [hello, isolate, this is args]
main isolate message: [0, SendPort]
isolate_1 stop
main isolate message: [1, isolate_1 任务完成]
isolate_1 message: [1, 这条信息是 main isolate 发送的]

在这里插入图片描述
整个消息通讯过程如上图所示,两个Isolate是经过两对Port对象通讯,一对Port分别由用于接收消息的ReceivePort对象,和用于发送消息的SendPort对象构成。其中SendPort对象不用单首创建,它已经包含在ReceivePort对象之中。须要注意,一对Port对象只能单向发消息,这就如同一根自来水管,ReceivePortSendPort分别位于水管的两头,水流只能从SendPort这头流向ReceivePort这头。所以,两个Isolate之间的消息通讯确定是须要两根这样的水管的,这就须要两对Port对象。

理解了Isolate消息通讯的原理,那么在Dart代码中,具体是如何操做的呢?
在这里插入图片描述
ReceivePort对象经过调用listen方法,传入一个函数可用来监听并处理发送来的消息。SendPort对象则调用send()方法来发送消息。send方法传入的参数能够是null,num, bool, double,String, List ,Map或者是自定义的类。 在上例中,咱们发送的是包含两个元素的List对象,第一个元素是整型,表示消息类型,第二个元素则表示消息内容。

spawn

static Future<Isolate> spawn()

除了使用spawnUri,更经常使用的是使用spawn方法来建立新的Isolate,咱们一般但愿将新建立的Isolate代码和main Isolate代码写在同一个文件,且不但愿出现两个main函数,而是将指定的耗时函数运行在新的Isolate,这样作有利于代码的组织和代码的复用。spawn方法有两个必须的参数,第一个是须要运行在新Isolate的耗时函数,第二个是动态消息,该参数一般用于传送主IsolateSendPort对象。

spawn的用法与spawnUri类似,且更为简洁,将上面例子稍做修改以下

import 'dart:isolate'; 
import  'dart:io';

void main() {
  print("main isolate start");
  create_isolate();
  print("main isolate end");
}

// 建立一个新的 isolate
create_isolate() async{
  ReceivePort rp = new ReceivePort();
  SendPort port1 = rp.sendPort;

  Isolate newIsolate = await Isolate.spawn(doWork, port1);

  SendPort port2;
  rp.listen((message){
    print("main isolate message: $message");
    if (message[0] == 0){
      port2 = message[1];
    }else{
      port2?.send([1,"这条信息是 main isolate 发送的"]);
    }
  });
}

// 处理耗时任务
void doWork(SendPort port1){
  print("new isolate start");
  ReceivePort rp2 = new ReceivePort();
  SendPort port2 = rp2.sendPort;

  rp2.listen((message){
    print("doWork message: $message");
  });

  // 将新isolate中建立的SendPort发送到主isolate中用于通讯
  port1.send([0, port2]);
  // 模拟耗时5秒
  sleep(Duration(seconds:5));
  port1.send([1, "doWork 任务完成"]);

  print("new isolate end");
}

运行结果:

main isolate start
main isolate end
new isolate start
main isolate message: [0, SendPort]
new isolate end
main isolate message: [1, doWork 任务完成]
doWork message: [1, 这条信息是 main isolate 发送的]

不管是上面的spawn仍是spawnUri,运行后都会建立两个进程,一个是主Isolate的进程,一个是新Isolate的进程,两个进程都双向绑定了消息通讯的通道,即便新的Isolate中的任务完成了,它的进程也不会马上退出,所以,当使用完本身建立的Isolate后,最好调用newIsolate.kill(priority: Isolate.immediate);Isolate当即杀死。

Flutter 中建立Isolate

不管如何,在Dart中建立一个Isolate都显得有些繁琐,惋惜的是Dart官方并未提供更高级封装。可是,若是想在Flutter中建立Isolate,则有更简便的API,这是由Flutter官方进一步封装ReceivePort而提供的更简洁API。详细API文档

使用compute函数来建立新的Isolate并执行耗时任务

import 'package:flutter/foundation.dart';
import  'dart:io';

// 建立一个新的Isolate,在其中运行任务doWork
create_new_task() async{
  var str = "New Task";
  var result = await compute(doWork, str);
  print(result);
}


String doWork(String value){
  print("new isolate doWork start");
  // 模拟耗时5秒
  sleep(Duration(seconds:5));

  print("new isolate doWork end");
  return "complete:$value";
}

compute函数有两个必须的参数,第一个是待执行的函数,这个函数必须是一个顶级函数,不能是类的实例方法,能够是类的静态方法,第二个参数为动态的消息类型,能够是被运行函数的参数。须要注意,使用compute应导入'package:flutter/foundation.dart'包。

使用场景

Isolate虽好,但也有合适的使用场景,不建议滥用Isolate,应尽量多的使用Dart中的事件循环机制去处理异步任务,这样才能更好的发挥Dart语言的优点。

那么应该在何时使用Future,何时使用Isolate呢?
一个最简单的判断方法是根据某些任务的平均时间来选择:

  • 方法执行在几毫秒或十几毫秒左右的,应使用Future
  • 若是一个任务须要几百毫秒或之上的,则建议建立单独的Isolate

除此以外,还有一些能够参考的场景

  • JSON 解码
  • 加密
  • 图像处理:好比剪裁
  • 网络请求:加载资源、图片

参考资料:
Dart 文档
Isolate 文档

欢迎关注个人公众号:编程之路从0到1
编程之路从0到1