Dart的单线程模型&异步操作

摘要

单线程与异步操作并不冲突。

Dart是单线程的

程序中的耗时操作

开发中的耗时操作:
  • 在开发中,我们经常会遇到一些耗时的操作需要完成,比如网络请求、文件读取等等;

  • 如果我们的主线程一直在等待这些耗时的操作完成,那么就会进行阻塞,无法响应其它事件,比如用户的点击;

  • 显然,我们不能这么干!!

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

  • 处理方式一: 多线程,比如Java、C++,我们普遍的做法是开启一个新的线程(Thread),在新的线程中完成这些异步的操作,再通过线程间通信的方式,将拿到的数据传递给主线程。

  • 处理方式二: 单线程+事件循环,比如JavaScript、Dart都是基于单线程加事件循环来完成耗时操作的处理。不过单线程如何能进行耗时的操作呢?!

单线程的异步操作

  • 因为我们的一个应用程序大部分时间都是处于空闲的状态的,并不是无限制的在和用户进行交互。

  • 比如等待用户点击、网络请求数据的返回、文件读写的IO操作,这些等待的行为并不会阻塞我们的线程;

  • 这是因为类似于网络请求、文件读写的IO,我们都可以基于非阻塞调用;

阻塞式调用和非阻塞式调用

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。

  • 阻塞式调用: 调用结果返回之前,当前线程会被挂起,调用线程只有在得到调用结果之后才会继续执行。

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

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

  • 比如网络请求本身使用了Socket通信,而Socket本身提供了select模型,可以进行非阻塞方式的工作;

  • 比如文件读写的IO操作,我们可以使用操作系统提供的基于事件的回调机制;

这些操作都不会阻塞我们单线程的继续执行,我们的线程在等待的过程中可以继续去做别的事情。

单线程是如何来处理网络通信、IO操作它们返回的结果呢?答案就是事件循环(Event Loop)

总结

官方明确说明,Dart是个单线程语言,这很容易让人困惑。

并发编程长期以来有两种范式,

  • 一种是基于共享内存的,主要是多线程编程;
  • 一种是基于消息的,如Actor、CSP模型。
    从这个角度看,Isolate其实是消息驱动的并发编程,算是CSP模型的简化,跟多线程编程是完全不同的并发编程范式。

Isolate它在底层其实就是个线程,但是Dart VM 限制了Isolate的能力,使得Isolate之间不能直接共享内存且独立GC,只能通过Port机制收发消息。Port发送数据时是Copy的,如果有大块内存真的要Copy多份,可能会有比较大的内存问题。

在Dart VM中,有很多个这样的Isolate,其中有一个Root Isolate是在Engine启动时创建的,它负责UI渲染以及用户交互操作,需要及时响应,当存在耗时操作,则必须创建新的Isolate,否则UI渲染会被阻塞。

UI Task Runner被Flutter Engine用于执行Dart root Isolate代码,引擎启动的时候为其增加了必要的绑定,使其具备调度提交渲染帧的能力。对于每一帧,引擎要做的事情有:

  1. Root isolate通知Flutter Engine有帧需要渲染;

  2. Flutter Engine通知平台,需要在下一个vsync的时候得到通知;

  3. 平台等待下一个vsync;

  4. 对创建的对象和Widgets进行Layout并生成一个Layer Tree,这个Tree马上被提交给Flutter Engine。当前阶段没有进行任何光栅化,这个步骤仅是生成了对需要绘制内容的描述。

  5. 创建或者更新Tree,这个Tree包含了用于屏幕上显示Widgets的语义信息。这个东西主要用于平台相关的辅助Accessibility元素的配置和渲染。

既然Flutter Engine有自己的Runner,那为何还要Dart的Isolate?
  • Dart isolate跟Flutter Runner是相互独立的,他们通过任务调度机制相互协作。
  • Dart的Isolate是Dart虚拟机自己管理的,Flutter Engine无法直接访问。Root Isolate通过Dart的C++调用能力把UI渲染相关的任务提交到UI Runner执行这样就可以跟Flutter Engine相关模块进行交互。

参考

  1. Dart 异步编程
  2. Flutter(五)之彻底搞懂Dart异步
  3. flutter入门