Dart之异步编程和future对象

文章目录

  • 重中之重
  • 异步编程
    • 1.事件循环体系
      • 1.1.Event-Looper
      • 1.2.单线程模型
      • 1.3.Dart的消息循环和消息队列(重中之重)
      • 1.4.通过链接方式指定任务顺序
      • 1.5Dart 的事件循环
    • Future类
      • **错误处理**
  • Isolate(隔离) 协程
      • 代码演示:spawnUri和spawn(更常用)
    • Flutter中创建Isolate
    • 使用场景

重中之重

注:如果async函数没有明确指定返回值,返回的null值的Future比如函数是打印 而不是return;
async 不是并行执行,它是遵循Dart 事件循环规则来执行的,它仅仅是一个语法糖,简化Future API的使用。

  1. Future中的then并没有创建新的Event丢到Event Queue中,而只是一个普通的Function Call,在FutureTask执行完后,立即开始执行

  2. 当Future在then函数先已经执行完成了,则会创建一个task,将该task的添加到microtask queue中,并且该任务将会执行通过then传入的函数

  3. Future只是创建了一个Event,将Event插入到了Event Queue的队尾

  4. 使用Future.value构造函数的时候,就会和第二条一样,创建Task丢到microtask Queue中执行then传入的函数

  5. Future.sync构造函数执行了它传入的函数之后,也会立即创建Task丢到microtask Queue中执行

异步编程

**Dart是单线程模型,什么是单线程模型呢?**单线程就是在程序执行时,所走的程序路径按照连续顺序排列下来,前面的必须处理好,后面的才会执行(就是同一个时刻只能执行一个操作)。

1.事件循环体系

1.1.Event-Looper

Dart是单线程模型,并没有主线程/子线程之分,Dart是Event loops和Event Queue模型,而EventLooper将所有的事件依次执行。

当产生一个Event之后,会进入Event queue,而Event loop从EventQueue中获取Event并且处理。

1.2.单线程模型

当一个Dart函数开始执行,那么它就会执行到这个函数结束,也就是函数不会被其他代码所打断。这里首先解释一下什么是Dart中的isolate(协程)。isolate本身是隔离的意思,有自己的内存和单线程控制的实体,因为isolate之间的内存在逻辑是隔离的,isolate的代码是按顺序执行的。在Dart中并发可以使用用isolate,isolate和Thread很像,但是isolate之间没有共享内存。一个Dart程序是在Main isolate的Main函数开始,我们平时开发中,默认环境就是Main isolate,App的启动入口main函数就是一个isolate,在Main函数结束后,Main isolate线程开始一个一个处理Event Queue中的每一个Event。

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

我们很容易发现,这种基于事件的异步模型,只适合I/O密集型的耗时操作,因为I/O耗时操作,往往是把时间浪费在等待对方传送数据或者返回结果,因此这种异步模型往往用于网络服务器并发。如果是计算密集型的操作,则应当尽可能利用处理器的多核,实现并行计算。

1.3.Dart的消息循环和消息队列(重中之重)

一个DartMain isolate只有一个消息循环(Event Looper)和两个消息队列:Event队列和MicroTask队列。

  1. Event队列包含所有外来事件:I/O,mouse events(鼠标事件),drawing events(绘图),timers(计时器),isolate之间的message等
  2. microTask队列在Dart中是很有必要的,因为有时候事件处理想要在稍后完成一些任务但又希望是在执行下一个事件消息之前这个才是重中之重

Event队列包含Dart和系统中其他位置的事件,MicroTask只包含Dart的代码,那么Event Looper处理两个队列的顺序是: 当main方法退出后,Event Looper就开始它的工作,首先会以FIFO的顺序执行MicroTask(先执行简短的异步任务),当所有的microtask执行完就会从Event队列去提取事件执行,这样反复,直到两个队列都是空。
Dart之异步编程和future对象_第1张图片
这里要注意:**虽然可以预测知道任务执行顺序,但是无法准确预测事件循环什么时候处理期望的任务。**就好像创建了一个延时2s的任务,但是排在你之前的任务结束前事件处理是不会处理这个延时2s任务,也就是执行这个延时任务有可能大于2s。

1.4.通过链接方式指定任务顺序

new Future(() => futureTask)  //异步任务的函数
        .then((d) => "execute value:$d")  //任务执行完后的子任务
        .then((d) => d.length)  //其中d为上个任务执行完后的返回的结果
        .then((d) => printLength(d))
        .whenComplete(() => whenTaskCompelete);  //当所有任务完成后的回调函数
}

1.5Dart 的事件循环

Dart 是事件驱动的体系结构,该结构基于具有单个事件循环和两个队列的单线程执行模型。

Dart虽然提供调用堆栈。 但是它使用事件在生产者和消费者之间传输上下文。 事件循环由单个线程支持,因此根本不需要同步和锁定。

Dart 的两个队列分别是

  1. MicroTask queue 微任务队列

  2. Event queue 事件队列

Dart中的Main Isolate只有一个Event Looper,但是存在两个Event Queue:Event Queue以及Microtask Queue

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

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

Future类

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

//我的任务
void  myTask(){
    print("this is my task");
}

void  main() {
    Future fut = new  Future(myTask);//根据我的任务创建Future对象
}

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

什么是Future

Future表示在将来某时获取一个值的方式。当一个返回Future的函数被调用的时候,做了两件事情:

  1. 函数把自己放入队列和返回一个未完成的Future对象
  2. 之后当值可用时,Future带着值变成完成状态。
    为了获得Future的值,有两种方式:
  3. 使用async和await
  4. 使用Future的接口

async和await关键字是Dart异步支持的一部分。他们允许你像写同步代码一样写异步代码和不需要使用Future接口。
注:在Dart2中有轻微的改动。async函数执行时,不是立即挂起,而是要执行到函数内部的第一个await。在多数情况下,我们不会注意到这一改动

// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';

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

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

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');
}

const news = '';
Duration oneSecond = const Duration(seconds: 1);

final newsStream = new Stream.periodic(oneSecond, (_) => news);

// Imagine that this function is more complex and slow. :)
Future gatherNewsReports() => newsStream.first;

// Alternatively, you can get news from a server using features
// from either dart:io or dart:html. For example:
//
// import 'dart:html';
//
// Future gatherNewsReportsFromServer() => HttpRequest.getString(
//      'https://www.dartlang.org/f/dailyNewsDigest.txt',
//    );

从执行结果我们可以注意到printDailyNewsDigest是第一个调用的,但是新闻是最后才打印,即使只要一行内容。这是因为代码读取和打印内容是异步执行的。

在这个例子中间,printDailyNewsDigest调用的gatherNewsReports不是阻塞的。gatherNewsReports把自己放入队列,不会暂停代码的执行。程序打印中奖号码,天气预报和比赛分数;当gatherNewsReports完成收集新闻过后打印。gatherNewsReports需要消耗一定时间的执行完成,而不会影响功能:用户在读取新闻之前获得其他消息。

  1. 开始程序执行
  2. main函数调用printDailyNewsDigest,因为它被标记为async,所有在该函数任何代码被执行之前立即返回一个Future。
  3. 剩下的打印执行。因为它们是同步的。所有只有当一个打印函数执行完成过后才能执行下一个打印函数。例如:中奖号码在天气预报执行打印。
  4. 函数printDailyNewsDigest函数体开始执行
  5. 在到达await之后,调用gatherNewsReports,程序暂停,等待gatherNewsReports返回的Future完成。
  6. 当Future完成,printDailyNewsDigest继续执行,打印新闻。
  7. 当printDailyNewsDigest执行完成过后,最开始的Future返回完成,程序退出。

重中之重
注:如果async函数没有明确指定返回值,返回的null值的Future

错误处理

如果在Future返回函数发生错误,你可能想捕获错误。Async函数可以用try-catch捕获错误。

printDailyNewsDigest() async {
  try {
    String news = await gatherNewsReports();
    print(news);
  } catch (e) {
    // Handle error...
  }
}

ry-catch在同步代码和异步代码的表现是一样的:如果在try块中间发生异常,那么在catch中的代码执行

连续执行

你可以使用多个await表达式,保证一个await执行完成过后再执行下一个

// Sequential processing using async and await.
main() async {
  await expensiveA();
  await expensiveB();
  doSomethingWith(await expensiveC());
}

expensiveB函数将等待expensiveA完成之后再执行。

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");
}

除了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]

Isolate(隔离) 协程

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

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

**isolate本身是隔离的意思,有自己的内存和单线程控制的实体,因为isolate之间的内存在逻辑是隔离的,isolate的代码是按顺序执行的。在Dart中并发可以使用用isolate,**isolate和Thread很像,但是isolate之间没有共享内存。一个Dart程序是在Main isolate的Main函数开始,我们平时开发中,默认环境就是Main isolate,App的启动入口main函数就是一个isolate,在Main函数结束后,Main isolate线程开始一个一个处理Event Queue中的每一个Event。

整个消息通信过程:

两个Isolate是通过两对Port对象通信,一对Port分别由用于接收消息的ReceivePort对象,和用于发送消息的SendPort对象构成。

其中SendPort对象不用单独创建,它已经包含在ReceivePort对象之中。

需要注意,一对Port对象只能单向发消息,这就如同一根自来水管,ReceivePort和SendPort分别位于水管的两头,水流只能从SendPort这头流向ReceivePort这头。因此,两个Isolate之间的消息通信肯定是需要两根这样的水管的,这就需要两对Port对象。

代码演示:spawnUri和spawn(更常用)

a、spawnUri用法

spawnUri方法有三个必须的参数,

  1. 第一个是Uri,指定一个新Isolate代码文件的路径,
  2. 第二个是参数列表,类型是List,
  3. 第三个是动态消息。

需要注意,用于运行新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 发送的]

b、spawn用法

spawn更常用:

我们通常希望将新创建的Isolate代码和main Isolate代码写在同一个文件,且不希望出现两个main函数

而是将指定的耗时函数运行在新的Isolate,这样做有利于代码的组织和代码的复用

spawn方法有两个必须的参数,第一个是需要运行在新Isolate的耗时函数,第二个是动态消息,该参数通常用于传送主Isolate的SendPort对象。

spawn的用法:

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中的任务完成了,它也不会立刻退出,因此,当使用完自己创建的Isolate后,最好调用newIsolate.kill(priority: Isolate.immediate);将Isolate立即杀死。

Flutter中创建Isolate

在Dart中创建一个Isolate都显得有些繁琐,Dart官方并未提供更高级封装
但是,如果想在Flutter中创建Isolate,则有更简便的API
compute函数

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);
}


void 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,多线程,dart)