Flutter - Dart中的异步编程及多线程

Flutter - Dart中的异步编程及多线程

前言


生命在于不断学习,探索未知的世界!!!


今天我们将从Future出发,一步一步探索Dart中的异步编程及相关的多线程知识。之前看网上一些文章说Dart没有多线程,那到底是不是这样呢?今天我们一探究竟!
本文涉及关键词:

  • Dart的事件循环
  • Future 、async和await等
  • isolate
  • compute

Dart的事件循环

Dart中,实际上有两种队列:

  1. 事件队列(event queue):包括所有的外来事件,I/Omouse eventsdrawing eventstimersisolate之间的信息传递。
  2. 微任务队列(microtask queue):表示一个短时间内就会完成的异步任务。它的优先级最高,高于event queue,只要队列中还有任务,就可以一直霸占着事件循环。microtask queue添加的任务主要是由Dart内部产生。

怎么理解这个呢?我们可以借鉴下面这张图看看Dart的事件循环所谓的优先级是怎样的一个过程。


上图可见,在每一次事件循环中,Dart总是先去第一个microtask queue中查询是否有可执行的任务,如果没有,才会处理后续的event queue的流程。

在我们日常开发用的多的还是优先级低的事件队列Dartevent queue的任务建立提供了一层封装,就是我们在Dart中经常用到的Future

正常情况下,一个 Future 异步任务的执行是相对简单的:

  1. 声明一个 Future 时,Dart 会将异步任务的函数执行体放入event queue,然后立即返回,后续的代码继续同步执行

  2. 当同步执行的代码执行完毕后,event queue会按照加入event queue的顺序(即声明顺序),依次取出事件,最后同步执行 Future 的函数体及后续的操作。

Future 、async和await

首先我们可以引入一个简单的例子,来看看这个Future的基本使用:

void main() {
  getData();
  print('做其他事情');
}
String _data = '0';
void getData() {
  print('开始data = $_data');
  Future(() {
    //耗时操作
    for (int i = 0; i < 1000000000; i++) {
      _data = '网络数据';
    }
  });
  print('结束data=$_data');
}

下面是运行打印结果:


Future异步执行

可以看出,在当前线程中,Future 所包含的任务是异步执行的,先进行Future之后的任务,再执行Future内的函数,如果我们想要同步执行的话,也就是先执行Future 内的函数再执行之后的事件,可用通过关键字asyncawait:

void main() {
  getData();
  print('做其他事情');
}
String _data = '0';
void getData() async {
  print('开始data = $_data');
  await Future(() {
    //耗时操作
    for (int i = 0; i < 1000000000; i++) {
      _data = '网络数据';
    }
  });
  print('结束data=$_data');
}
Future同步执行

可以看到,此时的Future所包含的任务就是同步执行,await 会等待Future执行结束后,才会继续执行后面的代码。而且在添加asyncawait 后,当前getData函数直接返回了,立刻就执行了main函数内后续任务,所以可见添加了async的函数对当前线程后续操作是没有阻拦效果的,提高程序运行效率。

此时,在添加async函数之后,getData这整个函数已经是个异步函数,getData会返回一个Future对象。

异步函数即在函数头中包含关键字async的函数。

  • async: 用来表示函数是异步的,定义的函数会返回一个Future对象。
  • await:后面跟着一个Future,表示等待该异步任务完成,异步任务完成后才会继续往下执行。await只能出现在异步函数内部(await单独存在是没有意义的,要配合async使用)。能够让我们可以像写同步代码那样来执行异步任务而不使用回调的方式。

Future.then()

我们先来看看这个then()的用法:

void main() {
  getData();
  print('做其他事情');
}
String _data = '0';
void getData() {
  print('开始data = $_data');
  Future future = Future(() {
    //耗时操作
    for (int i = 0; i < 1000000000; i++) {
      _data = '网络数据';
    }
  });
  future.then((value) => print('结束data=$_data'));
  
  print('再多干点事情');
}

then使用

因为Future 返回的是一个Future对象,获取这个对象,调用then方法,我们发现then里面的任务是在Future任务块结束后执行的。
不妨再多加两个then方法的调用:

  future.then((value) => print('结束data=$_data'));
  future.then((value) => print('结束1data=$_data'));
  future.then((value) => print('结束2data=$_data'));
多个then方法使用

由此可知,then方法是用来注册一个Future完成时要调用的回调。如果 Future 有多个then,它们也会按照链接的先后顺序同步执行,同时也会共用一个event loop。
查看then方法源码可知then方法最终返回也是一个Future对象:

Future<R> then<R>(FutureOr<R> onValue(T value), {Function? onError});

也就是说then方法可以链式调用:

future.then((value) => print('结束data=$_data'))
      .then((value) => print('结束1data=$_data'))
      .then((value) => print('结束2data=$_data'));

打印结果和上面一样一样的。

综合型例子

下面我们利用一个综合型例子来看事件队列、微任务队列及then方法之间的关系:

void testFuture() {
  Future x1 = Future(() => null);
  Future x2 = x1.then((value) {
    print('6');
    scheduleMicrotask(() => print('7'));
  });
  x2.then((value) => print('8'));

  Future x = Future(() => print('1'));
  x.then((value) {
    print('4');
    Future(() => print('9'));
  }).then((value) => print('10'));

  Future(() => print('2'));

  scheduleMicrotask(() => print('3'));

  print('5');
}

执行结果打印:5 3 6 8 7 1 4 10 2 9

分析:

  1. 首先调用testFuture()函数,所以优先打印 5
  2. 执行优先级较高的微任务事件3
  3. 然后按照Future的声明顺序执行,打印6。此时只是将7这个微任务添加到队列中,暂时还没轮到7执行。
  4. 打印完6后,继续立即执行x2.then方法,所以打印8
  5. 打印完8之后,此时队列里有添加进来的优先级高的7,所以打印7
  6. 然后继续向下执行,打印1,接着4,接着就是10。同样,此处的9只是添加进event queue,并不执行。
  7. 你以为此时会打印刚添加进去的9,不不不!要知道任务2是在一开始打印5之前就添加进了event queue, 所以2应该在9之前,10之后打印。9才是最后被添加进事件队列的,所以最后打印。

结论

  • then() 函数是前面Future函数体执行结束后立即执行的,可以看作是一个微任务,优先级高。
  • 微任务队列(microtask queue)优先级高于事件队列(event queue)。

Future.catchError()

顾名思义,catchError()是用来捕捉Future抛出的错误的。

String _data = '0';
void getData() {
  print('开始data = $_data');
  Future future = Future(() {
    //耗时操作
    for (int i = 0; i < 1000000000; i++) {}

    throw '网络错误';
  });
  future
      .then((value) => print('结束data=$value'))
      .catchError((e) => print('捕获到了:' + e.toString()));
  print('再多干点事情');
}

catchError使用

链式调用catchError()函数,最终捕获到了原始Future 所抛出的错误,而我们可以看到此时then()函数暂未打印,说明一旦原Future跑出错误,then()不会执行。

注意点catchError()尽量放在链式调用最后面。

then中的回调onError 和 Future.catchError

Future.catchError回调只处理原始Future抛出的错误,不能处理回调函数抛出的错误,onError只能处理当前Future的错误:

future.catchError((e) => print('捕获到了:' + e.toString()))
      .then((value) => print('结束data=$value'),onError: (e) => print('then出错误了'));
catchError放前面使用
future.then((value) => print('结束data=$value'),
          onError: (e) => print('then出错误了'))
      .catchError((e) => print('捕获到了:' + e.toString()));
catchError放后面使用

catchError源码:

Future<T> catchError(Function onError, {bool test(Object error)?});

上面两个例子可以看出:

  1. catchError()捕获原future抛出错误后,then()里的onError就不执行了,因为catchError源码可知,catchError()返回的也是一个Future ,且返回的Future无错误抛出,所以当调用then函数时,里面的onError就无错误可处理,不打印。
  2. 而当原Future先调用then()时,此时onError捕获到了原future抛出的错误,并处理打印。然后此情况下的链式调用then返回的Future 无错误抛出(即本次链式调用再无错误抛出),所以最后的catchError无错误处理,暂不打印。

Future.whenComplete()

Future.whenComplete()Future 完成之后总是会调用,不管是抛出错误还是正常执行结束。

future
      .then((value) => print('结束data=$value'))
      .whenComplete(() => print('完成了'))
      .catchError((e) => print('捕获到了:' + e.toString()));
whenComplete放前面
future
      .then((value) => print('结束data=$value'))
      .catchError((e) => print('捕获到了:' + e.toString()))
      .whenComplete(() => print('完成了'));
whenComplete放后面

所以,在Future链式调用中,只要前面的Future完成,whenComplete就会执行。即使是下面这种情况:

future
      .whenComplete(() => print('完成了'))
      .then((value) => print('结束data=$value'))
      .catchError((e) => print('捕获到了:' + e.toString()));
whenComplete放原Future后面

所以,只要当前的Future函数执行体结束,whenComplete就会执行。

Future.wait()

wait()我们只简单介绍下用法:

void waitDemo() {
  Future.wait([
    Future(() {
      print('1 执行');
      return '任务1 ';
    }),
    Future(() {
      print('2 执行');
      return '任务2 ';
    }),
    Future(() {
      print('3 执行');
      return '任务3 ';
    })
  ]).then((value) => print(value[0] + value[1] + value[2]));
}
wait()使用

简单理解就是,wait()中的Future执行体依次先执行,然后最后再通过then去处理所有的Future返回的结果。

Dart中的多线程

首先我们从一个简单Future异步任务入手:

void isolateDemo() {
  print('1');
  Future(func);
  sleep(Duration(seconds: 2));
  print('2');
}
func() => print('3');
Future异步

Future 是个异步任务,所以func的执行一定需要等待2任务结束再执行,因为都在一个主线程里。下面我们引入Isolate()

void isolateDemo() {
  print('1');
  Isolate.spawn(func, 10);
  sleep(Duration(seconds: 2));
  print('2');
}
func(int count) => print('3');

Isolate使用

看到没,打印顺序变成了1 3 2,说明3任务没有被2任务所阻拦,也就是说3 任务不在主队列里面执行,而是在子线程里执行。
我们再把代码复杂点:

void isolateDemo() {
  print('1');
  Isolate.spawn(func, 10);
  Isolate.spawn(func1, 10);
  Isolate.spawn(func, 10);
  Isolate.spawn(func1, 10);
  Isolate.spawn(func, 10);
  Isolate.spawn(func1, 10);
  Isolate.spawn(func, 10);
  Isolate.spawn(func1, 10);
  Isolate.spawn(func, 10);
  Isolate.spawn(func1, 10);
  Isolate.spawn(func, 10);
  Isolate.spawn(func1, 10);
  sleep(Duration(seconds: 2));
  print('2');
}
func(int count) => print('第一个来了');
func1(int count) => print('第二个来了');

Isolate使用2

打印结果可见,func()func1()两个函数执行顺序是随机的,也即是说随机开辟了子线程去执行。
Isolate 可以结合端口(port)使用:

void isolateDemo() async {
  print('1');
  //创建一个port
  ReceivePort port = ReceivePort();
  //创建Isolate
  Isolate iso = await Isolate.spawn(func, port.sendPort);
  port.listen((message) {
    a = message;
    print('接收到了$a');
    port.close();
    iso.kill();
  });
  sleep(Duration(seconds: 2));
  print('a = $a');
}
int a = 10;
func(SendPort send) {
  send.send(100);
}

可实现在子线程中修改变量,但是主线程中的变量还是不变。
注意:端口记得关闭port.close(),子线程需要手动杀死iso.kill()

Dart中的Compute()

Dart中还有另外一个Compute可以调起子线程:

void computeDemo() {
  print('1');
  compute(func2, 10);
  sleep(Duration(seconds: 2));
  print('2');
}
func2(int count) => print('3');

Compute使用

打印结果可见,3任务同样也是在子线程中进行的。

异步结合多线程使用

void computeFuncDemo() {
  Future(() => compute(funcC, 123)).then((value) => print('1结束'));
  Future(() => compute(funcC, 123)).then((value) => print('2结束'));
  Future(() => compute(funcC, 123)).then((value) => print('3结束'));
  Future(() => compute(funcC, 123)).then((value) => print('4结束'));
  Future(() => compute(funcC, 123)).then((value) => print('5结束'));
}

funcC(int message) {}

异步多线程1

此时的异步任务中开启了子线程,=> 符号的意思是返回,所以then()的调用也是在子线程中进行。
我们再改一下代码:

void computeFuncDemo() {
  Future(() {
    compute(funcC, 123);
  }).then((value) => print('1结束'));
  Future(() {
    compute(funcC, 123);
  }).then((value) => print('2结束'));
  Future(() {
    compute(funcC, 123);
  }).then((value) => print('3结束'));
  Future(() {
    compute(funcC, 123);
  }).then((value) => print('4结束'));
  Future(() {
    compute(funcC, 123);
  }).then((value) => print('5结束'));
}

funcC(int message) {}

异步多线程2

改过的代码居然顺序执行,这不是偶然,因为此时的then的调用者是Future本身,此处没有=>符号里的返回的意思,所以此时按声明顺序打印。
当在所有compute 之前加上return,结果又不一样了:
异步多线程3

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,723评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,003评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,512评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,825评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,874评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,841评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,812评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,582评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,033评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,309评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,450评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,158评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,789评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,409评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,609评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,440评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,357评论 2 352

推荐阅读更多精彩内容