likes
comments
collection
share

flutter线程简述

作者站长头像
站长
· 阅读数 6

在开发中,我们经常会遇到一些耗时的操作需要完成,比如网络请求、文件读取等等; 如果我们的主线程一直在等待这些耗时的操作完成,那么就会进行阻塞,无法响应其它事件,比如用户的点击;显然,我们不能这么干!! 如何处理耗时的操作呢?

针对如何处理耗时的操作,不同的语言有不同的处理方式。

  • 处理方式一: 多线程,比如Java、C++,我们普遍的做法是开启一个新的线程(Thread),在新的线程中完成这些异步的操作,再通过线程间通信的方式,将拿到的数据传递给主线程。
  • 处理方式二: 单线程+事件循环,比如JavaScript、Dart都是基于单线程加事件循环来完成耗时操作的处理。

单线程如何能进行耗时的操作呢??

  • 阻塞式调用: 调用结果返回之前,当前线程会被挂起,调用线程只有在得到调用结果之后才会继续执行。
  • 非阻塞式调用: 调用执行之后,当前线程不会停止执行,只需要过一段时间来检查一下有没有结果返回即可。

而我们开发中的很多耗时操作,都可以基于这样的 非阻塞式调用:

比如网络请求本身使用了Socket通信,而Socket本身提供了select模型,可以进行非阻塞方式的工作; 比如文件读写的IO操作,我们可以使用操作系统提供的基于事件的回调机制;

一、事件循环(event loop)和事件队列(event queue)

Flutter App应用程序启动后,开始执行main函数并运行main isolate

flutter线程简述

isolate包含一个事件循环(event loop)以及两个事件队列,event queue和microtask queue事件队列,event和microtask队列有点类似iOS的source0和source1。source0处理本线程内任务优先级高,source1处理系统通过port传递的任务

event queue:负责处理I/O事件、绘制事件、手势事件、接收其他isolate消息等外部事件。 microtask queue:可以自己向isolate内部添加事件,事件的优先级比event queue高,可以通过scheduleMicrotask添加microtask。

当我们有一些事件时,比如点击事件、IO事件、网络事件时,它们就会被加入到event queue中,当发现事件队列不为空时发现,就会取出事件,并且执行。

flutter线程简述

RaisedButton(
  child: Text('Click me'),
  onPressed: () {
    final myFuture = http.get('https://example.com');
    myFuture.then((response) {
      if (response.statusCode == 200) {
        print('Success!');
      }
    });
  },
)

flutter线程简述

二、异步操作

2.1 Future

先看一个小例子

import "dart:io";

main(List<String> args) {
  print("main function start");
  print(getNetworkData());
  print("main function end");
}

String getNetworkData() {
  sleep(Duration(seconds: 3));
  return "network data";
}

main function start
// 等待3
network data
main function end

主线程被卡顿的3秒什么都干不了,这很明显是不行的,那么我们借助Future来修改一下

import "dart:io";

main(List<String> args) {
  print("main function start");
  final result = getNetworkData(); //马上就可以返回一个Future<String>的对象,状态为未完成
  print(result);
  result.then((value){
      print(value);
  }).catchError((e){
      print(e);
    }).whenComplete((){
      print('完事儿了');
    });
  print("main function end");
}

Future<String> getNetworkData() {
  return Future<String>(() {
    sleep(Duration(seconds: 3));
    return "network data";
//    throw Exception('报错了');
  });
}

执行结果

main function start
Instance of 'Future<String>'
main function end
//等待3
network data
完事儿了

future通过.then处理正常返回数据的情况,通过..catchError函数来处理异常情况,通过.whenComplete函数来处理执行结束的回调,

2.1.1 Future链式调用
import "dart:io";

main(List<String> args) {
  print("main function start");

  getNetworkData().then((value1) {
    print(value1);
    return "content data2";
  }).then((value2) {
    print(value2);
    return "message data3";
  }).then((value3) {
    print(value3);
  });//这里还可以继续.then吗??

  print("main function end");
}

Future<String> getNetworkData() {
  return Future<String>(() {
    sleep(Duration(seconds: 3));
     return "network data1";
  });
}

2.1.2 Future其他API

Future.value(value) 表示生成一个状态为已完成的future对象,和.then函数一起作为事件插入event queue中等待eventloop轮询时执行

main(List<String> args) {
  print("main function start");

  Future.value("哈哈哈").then((value) {
    print(value);
  });

  print("main function end");
}

执行结果

main function start
main function end
哈哈哈

Future.error(object)Future.value(value)类似

2.1.3 Future.delayed(时间, 回调函数)

我们可以使用Timer实现延时任务,也可以使用Future.delayed来实现,使用Future.delayed可以更好的处理异常情况,是在Timer的基础上进行了优化封装。

main(List<String> args) {
  print("main function start");

  Future.delayed(Duration(seconds: 3), () {
    return "3秒后的信息";
  }).then((value) {
    print(value);
  });

  print("main function end");
}
2.1.4 Future综合练习
  • Future没有执行完成(有任务需要执行),那么then会直接被添加到Future的函数执行体后;
// future_1加入到eventqueue中,紧随其后then_1被加入到eventqueue中
Future(() => print("future_1")).then((_) => print("then_1"));
  • 如果Future执行完后就then,该then的函数体被放到如微任务队列,当前Future执行完后执行微任务队列
// Future没有函数执行体,then_2被加入到microtaskqueue中
Future(() => null).then((_) => print("then_2"));
  • 如果Future世链式调用,意味着then未执行完,下一个then不会执行
// future_3、then_3_a、then_3_b依次加入到eventqueue中
Future(() => print("future_3")).then((_) => print("then_3_a")).then((_) => print("then_3_b"));
import "dart:async";

main(List<String> args) {
  print("main start");

  Future(() => print("task1"));
	
  final future = Future(() => null);

  Future(() => print("task2")).then((_) {
    print("task3");
    scheduleMicrotask(() => print('task4'));
  }).then((_) => print("task5"));

  future.then((_) => print("task6"));
  scheduleMicrotask(() => print('task7'));

  Future(() => print('task8'))
    .then((_) => Future(() => print('task9')))
    .then((_) => print('task10'));

  Future(() => print("task11"));
  print("main end");
}

执行结果

main start
main end
task7
task1
task6
task2
task3
task5
task4
task8
task11
task9
task10

如果future f1的运算返回另一个future f2呢?如果结果本身是future,则f1不会被完成。我们可能在onValue被调用时,要被迫检测传入的参数是否是future,并通过另一个then()来处理,等等,无限循环。幸好,在这种情况下,f1的完成值取决于f2的完成值。当f2的完成值不是future时,f1将以相同的值完成。因此我们说future是链式的,因为他们形成了一个依赖链。

练习2

void testFuture() {
  Future f1 = new Future(() => print('f1'));
  Future f2 = new Future(() =>  null);
  Future f3 = new Future.delayed(Duration(seconds: 1) ,() => print('f2'));
  Future f4 = new Future(() => null);
  Future f5 = new Future(() => null);

  f5.then((_) => print('f3'));
  f4.then((_) {
    print('f4');
    new Future(() => print('f5'));
    f2.then((_) {
      print('f6');
    });
  });
  f2.then((m) {
    print('f7');
  });
  print('f8');
}

输出结果

com.example.flutter_dart_app I/flutter: f8
com.example.flutter_dart_app I/flutter: f1
com.example.flutter_dart_app I/flutter: f7
com.example.flutter_dart_app I/flutter: f4
com.example.flutter_dart_app I/flutter: f6
com.example.flutter_dart_app I/flutter: f3
com.example.flutter_dart_app I/flutter: f5
com.example.flutter_dart_app I/flutter: f2

2.2 await、async

正如我们所看到的,直接与future交互可能有些尴尬。部分原因是,我们熟悉的经典控制结构并没有为异步做考虑。一旦进行了异步调用,跟踪调用是否已执行、执行是否成功以及结果是什么等所有必要工作都成为程序员的责任。安排future进行工作,在其工作完成时安排后续工作,确定工作成功或不成功等,这一系列任务相当繁重。 为了减轻使用异步操作的痛苦,Dart为异步函数提供了语言级支持。函数体可以使用async修饰符进行标记,标记后的函数是一个async函数。

Future<int> foo() async => 42

使用async函数可以在多个方面简化处理future和异步的任务。 当调用async函数时,函数的代码不会立即执行。相反,函数中的代码被安排在将来的某个时间执行。那函数将什么返回给调用者呢?一个函数体执行成功或失败时被完成的future。函数会自动生成该future,并立即返回给调用者。

await表达式允许我们像编写同步代码那样编写异步代码。执行await表达式允许我们在等待异步计算完成时暂停运行周围的函数。 await只能在异步函数中使用。Dart编译器不会在其它地方接受等待。

它们可以让我们用同步的代码格式,去实现异步的调用过程

Future<String> getNetworkData() async {
  var result = await Future.delayed(Duration(seconds: 3), () {
    return "network data";
  });

  return "请求到的数据:" + result;
}
2.2.1 协程

如果想要了解async、await的原理,就要先了解协程的概念,async、await本质上就是协程的一种语法糖。协程,也叫作coroutine,是一种比线程更小的单元。如果从单元大小来说,基本可以理解为进程->线程->协程。

协程分为无线协程和有线协程,

  • 无线协程在离开当前调用位置时,会将当前变量放在堆区,当再次回到当前位置时,还会继续从堆区中获取到变量。所以,一般在执行当前函数时就会将变量直接分配到堆区,而async、await就属于无线协程的一种。
  • 有线协程则会将变量继续保存在栈区,在回到指针指向的离开位置时,会继续从栈中取出调用。

2.3 isolate

isolate是有着自己内存和单线程控制的计算过程。属于isolate源于独立实体间的分离,因为isolate之间的内存逻辑上是分离的。isolate中的代码是按顺序运行的,任何并发都是运行多个isolate的结果。因为dart没有共享内存的并发,所以不需要锁并且没有发生竞争的可能性。 由于isolate没有共享内存,所以他们之间可以通讯的唯一方式是通过消息传递。dart中的消息传递总是异步的。

一个isolate有多个port。port是dart isolate间通讯的底层实现。Port有两种:send port和receive port。 receive port是一个接收消息的stream;send port则允许发送消息给isolate,更确切的说,它允许将消息发送给receive port。send port可以被receive port生成,它将把所有消息发送给对应的receive port。

我们已经知道Dart是单线程的,这个线程有自己可以访问的内存空间以及需要运行的事件循环; 我们可以将这个空间系统称之为是一个Isolate; 比如Flutter中就有一个Root Isolate,负责运行Flutter的代码,比如UI渲染、用户交互等等; 在 Isolate 中,资源隔离做得非常好,每个 Isolate 都有自己的 Event Loop 与 Queue,

Isolate 之间不共享任何资源,只能依靠消息机制通信,因此也就没有资源抢占问题。 但是,如果只有一个Isolate,那么意味着我们只能永远利用一个线程,这对于多核CPU来说,是一种资源的浪费。

如果在开发中,我们有非常多耗时的计算,完全可以自己创建Isolate,在独立的Isolate中完成想要的计算操作。

  loadData() async {
    // 通过spawn新建一个isolate,并绑定静态方法
    ReceivePort receivePort =ReceivePort();
    await Isolate.spawn(dataLoader, receivePort.sendPort);

    // 获取新isolate的监听port
    SendPort sendPort = await receivePort.first;
    // 调用sendReceive自定义方法
    List dataList = await sendReceive(sendPort, 'https://jsonplaceholder.typicode.com/posts');
    print('dataList $dataList');
  }

// isolate的绑定方法
  static dataLoader(SendPort sendPort) async{
    // 创建监听port,并将sendPort传给外界用来调用
    ReceivePort receivePort =ReceivePort();
    sendPort.send(receivePort.sendPort);

    // 监听外界调用
    await for (var msg in receivePort) {
      String requestURL =msg[0];
      SendPort callbackPort =msg[1];

      // 回调返回值给调用者
      callbackPort.send('信息');
    }
  }

// 创建自己的监听port,并且向新isolate发送消息
  Future sendReceive(SendPort sendPort, String url) {
    ReceivePort receivePort =ReceivePort();
    sendPort.send([url, receivePort.sendPort]);
    // 接收到返回值,返回给调用者
    return receivePort.first;
  }

在isolate中启动另一个isolate被称为spawning。生成isolate时需要指定一个库,isolate会从该库的main()方法开始执行,这个库被称为isolate的根库。 类Isolate提供了两种用于生成isolate的类方法:第一种是spawnUri(),它基于给定库的URI来产生一个isolate;第二种是spawn(),它根据当前isolate的根库生成一个isolate

疑问

Flutter作为一套跨平台的UI框架有自己的渲染机制,但是Flutter App最终要运行在Native平台上面,代码的执行、任务的执行、页面的渲染等操作最终还是要依赖Native平台CPU去运算,还是需要操作系统的线程管理机制去管理,那么Flutter的isolate和Native的Thread究竟是怎么在协作的??????????

三、Native进程和线程(iOS)

每一个App运行在操作系统上视为一个进程,有独立的内存空间不与其它进程共享,进程通过port通讯 app启动以后默认开启主线程,我们也可以手动开启子线程,每一个线程都有一个事件循环Runloop,负责处理任务(source0、source1等)

  • source0是来自本线程内的任务
  • source1是通过port发送过来的任务

UI渲染等操作必须在主线程执行,耗时任务(网络请求)要开启子线程去执行,拿到结果之后再回调主线撑,阻塞主线撑会造成页面卡顿无响应

四、Flutter线程管理与Dart Isolate机制

flutter线程简述

Flutter Engine要求Embeder提供四个Task Runner,Embeder指的是将引擎移植到平台的中间层代码。这四个主要的Task Runner包括:

flutter线程简述

4.1 Platform Task Runner

Flutter Engine的主Task Runner,类似于Android Main Thread或者iOS的Main Thread。但是需要注意他们还是有区别的。

一般来说,一个Flutter应用启动的时候会创建一个Engine实例,Engine创建的时候会创建一个线程供Platform Runner使用。

跟Flutter Engine的所有交互(接口调用)必须在Platform Thread进行,否则可能导致无法预期的异常。这跟iOS UI相关的操作都必须在主线程进行相类似。需要注意的是在Flutter Engine中有很多模块都是非线程安全的。

规则很简单,对于Flutter Engine的接口调用都需保证在Platform Thread进行。

阻塞Platform Thread不会直接导致Flutter应用的卡顿(跟iOS android主线程不同)。尽管如此,也不建议在这个Runner执行繁重的操作,长时间卡住Platform Thread应用有可能会被系统Watchdog强杀。

4.2 UI Task Runner Thread(Dart Runner)

UI Task Runner用于执行Dart root isolate代码(isolate我们后面会讲到,姑且先简单理解为Dart VM里面的线程)。Root isolate比较特殊,它绑定了不少Flutter需要的函数方法,以便进行渲染相关操作。对于每一帧,引擎要做的事情有:

Root isolate通知Flutter Engine有帧需要渲染。 Flutter Engine通知平台,需要在下一个vsync的时候得到通知。 平台等待下一个vsync 对创建的对象和Widgets进行Layout并生成一个Layer Tree,这个Tree马上被提交给Flutter Engine。当前阶段没有进行任何光栅化,这个步骤仅是生成了对需要绘制内容的描述。 创建或者更新Tree,这个Tree包含了用于屏幕上显示Widgets的语义信息。这个东西主要用于平台相关的辅助Accessibility元素的配置和渲染。 除了渲染相关逻辑之外Root Isolate还是处理来自Native Plugins的消息,Timers,Microtasks和异步IO等操作。Root Isolate负责创建管理的Layer Tree最终决定绘制到屏幕上的内容。因此这个线程的过载会直接导致卡顿掉帧。

4.3 GPU Task Runner

GPU Task Runner主要用于执行设备GPU的指令。UI Task Runner创建的Layer Tree是跨平台的,它不关心到底由谁来完成绘制。GPU Task Runner负责将Layer Tree提供的信息转化为平台可执行的GPU指令。GPU Task Runner同时负责绘制所需要的GPU资源的管理。资源主要包括平台Framebuffer,Surface,Texture和Buffers等。

一般来说UI Runner和GPU Runner跑在不同的线程。GPU Runner会根据目前帧执行的进度去向UI Runner要求下一帧的数据,在任务繁重的时候可能会告诉UI Runner延迟任务。这种调度机制确保GPU Runner不至于过载,同时也避免了UI Runner不必要的消耗。

建议为每一个Engine实例都新建一个专用的GPU Runner线程。

4.4 IO Task Runner

前面讨论的几个Runner对于执行流畅度有比较高的要求。Platform Runner过载可能导致系统WatchDog强杀,UI和GPU Runner过载则可能导致Flutter应用的卡顿。但是GPU线程的一些必要操作,例如IO,放到哪里执行呢?答案正是IO Runner。

IO Runner的主要功能是从图片存储(比如磁盘)中读取压缩的图片格式,将图片数据进行处理为GPU Runner的渲染做好准备。IO Runner首先要读取压缩的图片二进制数据(比如PNG,JPEG),将其解压转换成GPU能够处理的格式然后将数据上传到GPU。

获取诸如ui.Image这样的资源只有通过async call去调用,当调用发生的时候Flutter Framework告诉IO Runner进行加载的异步操作。

IO Runner直接决定了图片和其它一些资源加载的延迟间接影响性能。所以建议为IO Runner创建一个专用的线程。

Mobile平台上面每一个Engine实例启动的时候会为UI,GPU,IO Runner各自创建一个新的线程。所有Engine实例共享同一个Platform Runner和线程。

4.5 Flutter Engine Runners与Dart Isolate

Runner是一个抽象概念,我们可以往Runner里面提交任务,任务被Runner放到它所在的线程去执行,这跟iOS GCD的执行队列很像。我们查看iOS Runner的实现实际上里面是一个loop,这个loop就是CFRunloop,在iOS平台上Runner具体实现就是CFRunloop。被提交的任务被放到CFRunloop去执行。

Dart的Isolate是Dart虚拟机自己管理的,Flutter Engine无法直接访问。Root Isolate通过Dart的C++调用能力把UI渲染相关的任务提交到UI Runner执行这样就可以跟Flutter Engine相关模块进行交互,Flutter UI相关的任务也被提交到UI Runner也可以相应的给Isolate一些事件通知,UI Runner同时也处理来自App方面Native Plugin的任务。

所以简单来说Dart isolate跟Flutter Runner是相互独立的,他们通过任务调度机制相互协作。

flutter线程简述

flutter线程简述

参考文章

异步编程:使用 Future 和 async-await

Dart异步任务与消息循环机制

聊一聊Flutter线程管理与Dart Isolate机制

深入理解Flutter多线程

深入理解 Flutter 多线程 | 开发者说·DTalk