发布时间:2019年6月11日 - 5分钟阅读
更新:巧合的是,Flutter团队刚刚发布了一个片段。它直观漂亮地说明了非阻塞概念。在下面的文章中,声称Dart不是单线程,而这个视频却声称不是。在研究和讨论更多的时候,我就不写文章了。
TLDR:Flutter/Dart不是单线程;Dart的并发模型不是Java的线程;Future/Async/Await运行在同一个线程上,解决IO绑定的问题,而Dart的Isolate/Flutter的计算运行在不同的隔离(无共享内存)线程上,解决CPU绑定的问题。
误区一:Flutter/Dart是单线程的。
在网上,我看到有人在说Dart是单线程语言,因此Flutter也是单线程语言。我想说,说Dart是单线程语言在技术上是不对的。让我来分析一下。
任何一段Dart代码都是在单线程中执行的,这是事实。也就是说,任何一段代码,没有回调和等待关键字,都可以保证不间断的执行。
但是,在我看来,单线程语言是一种老旧的编程语言,无法利用多核处理器的优势。为了简单起见,我们说单线程语言是并发语言的反面(虽然并发不是并行)。换句话说,在多核处理器上,用并发语言正确编写的应用程序可以并行进行多项计算,利用多核,而用单线程语言编写的应用程序的性能并不比运行在单核上的应用程序好。
其实Dart使用Isolates支持多线程(有点,见下一个误区),有了这个,Flutter就可以在移动或桌面上充分利用现代多核处理器的优势。
误区二:Flutter/Dart的并发模型和Java的线程一样。
正如迷思1中所解释的,Dart使用Isolates支持多线程。就在Isolates的介绍中,有人说过
isolates[是]独立的工作者,它们类似于线程,但不共享内存,只通过消息进行通信。
基本上,它类似于最新的浏览器/JavaScript的Web Workers API或NodeJS的Worker Threads。补充一点,按照神话1中的定义,JavaScript在没有Web Workers或Worker Threads API之前,一直是单线程语言(Node的Cluster API是关于fork/process的,不是线程)。
有些人在遇到问题时,会想:"我知道了,我就用线程吧。"然后二他们就会出现erpoblesms。
因为Dart使用的是消息传递模式,而且没有共享内存/变量,所以几乎不需要锁或mutex。另外,我们还注意到
如果你试图在隔离体之间传递更复杂的对象,如Future或http.Response,你可能会遇到错误。
简单来说,大多数时候,Dart的隔离体之间的通信必须是可序列化的。
误区3:Future/Async/Await在单独的线程中运行代码,解决了所有阻塞问题。
完全不对。Future/Async/Await在同一个线程中运行代码。如果需要等待外部数据/事件,则应使用Dart的事件队列,但仍在同一线程中。因此,Future/Async/Await只解决了一半的阻塞问题。
为了更好的理解,我们应该退一步讲。编程中的阻塞可以分为2类,I/O绑定和CPU绑定。
I/O指的是运动中的数据,比如网络HTTP请求/响应、向本地存储读/写数据、线程/隔离区之间的消息传递(见迷思2)。比如我们发出一个HTTP请求,等待HTTP响应的时候,等待的时间可能是几秒钟,在这几秒钟里,如果我们的应用写得不好,可能会被阻塞,UI也会被冻结。
在Java/Kotlin或ObjC/Swift中,默认情况下,大部分IO绑定的操作都是阻塞的,开发者必须有意识地做一些额外的代码,通过各种技术让它 "脱离""主线程"。在Dart中,默认情况下,所有IO绑定的操作都是async并返回Future。开发者可以通过附加Sync前缀来选择使用阻塞变化,比如File的方法copy与copySync,lastAccessed与lastAccessedSync。
但是,Future/Async/Await并不能解除对CPU绑定操作的封锁。看看这个简单的例子
在这种情况下,使用Future/Async/Await其实会让性能差很多,比如差180倍(哦,调整param的时候要注意,veryLongRunningCpuBoundFunction的设计是有O(nⁿ)时间复杂度😈的),但并不能解决阻塞问题。如果你不相信,只需复制粘贴veryLongRunningCpuBoundFunction(阻塞版或异步版),然后在你的Flutter应用中运行,只要调用该函数,你的UI就会被完全冻结。
veryLongRunningCpuBoundFunction可能看起来不切实际的简单和/或愚蠢。实际上,常见的CPU绑定操作有。
- 矩阵乘法
- 密码学相关的
- 图像/音频/视频处理
- 序列化/反序列化
- 离线机器学习模型计算
- 压缩
- 正则表达式拒绝服务 - ReDoS
在这种情况下,我们应该使用我们在前面2个迷思中一直在讨论的工具,Isolate。Dart的Isolate API,在我看来,并不是最容易使用的。幸运的是,Flutter有一个实用函数compute,它基本上是Dart的Isolate的一个包装器,API要简单得多(但有些有限)。事实上,Flutter的例子是关于在后台解析JSON的(解析JSON被归入CPU绑定操作的序列化/反序列化情况)。另外值得注意的是,compute返回了一个Future,因为CPU绑定的代码(解析JSON)现在运行在不同的隔离区,而隔离区的通信(消息传递)是IO绑定的。
重新审视上面的例子,这里是运行代码而不使UI冻结的正确方法。
同样值得注意的是,compute返回的是Future,但veryLongRunningCpuBoundFunction本身返回的是一个基元数据类型(本例中为int)。
Dart的Isolate / Flutter的compute对于从iOS/Android背景来Flutter的人来说是极其重要的一点。
Dart's Isolate / Flutter的计算对于从iOS/Android背景来Flutter的人来说是一个极其重要的点。如上所述,iOS/Android传统上并不区分IO绑定和CPU绑定的阻塞操作,因此开发者总是有意识地手动使用相同的技术来处理任何阻塞操作(Android上的线程+Future/Callback/Stream,iOS上的GCD)。在Dart/Flutter中,所有IO绑定的阻塞操作都已经被语言用Future识别并解决了,但新手要注意Dart的Isolate/Flutter的计算,以避免UI被冻结。
结语
Flutter/Dart在技术上不是单线程的,尽管Dart代码是在单线程中执行的。Dart是一种默认的线程安全的、可并行的、完全无阻塞的、带有消息传递模式的语言,可以充分利用现代多核架构的优势,而不用担心锁和mutex的问题。Dart中的阻塞可以是I/O绑定的,也可以是CPU绑定的,分别应该由Future和Dart的Isolate/Flutter的计算来解决。