一:前言 - 关于多线程与异步


关于 Dart,我相信大家都知道Dart是一门单线程语言,这里说的单线程并不是说Dart没有或着不能使用多线程,而是Dart的所有API默认情况下都是单线程的。但大家也都知道Dart是有办法支持多线程和异步操作的,关于多线程和异步这两个概念是需要我们理清楚的,不能混淆它们的概念,给我们的理解造成困扰。

1、多线程是开辟另外一个线程来处理事件,每个线程都有单独的事件队列,互不影响,这个新线程和当前线程是并列执行的,有的共享数据空间有的不共享(比如Isolate)。

2、异步是不阻塞当前线程,将异步任务和当前线程的任务分开,异步任务后面的任务,不会等待异步任务执行完再执行,而是直接执行,与异步任务的回调没有关系,这样就不影响当前线程的执行,这就叫异步。

      接下来我们按照 事件队列 -- 异步 -- 多线程 这样的顺序整理我们这篇的内容。

二:事件队列


这个和iOS比较类似,在Dart的线程中也存在事件循环和消息队列的概念,在Dart的线程中包含一个事件循环以及两个事件队列,我们先说清楚两个事件队列,再来整理它的事件循环或着说是消息循环机制是什么样子的。

1、事件任务队列(Event Queue):负责处理I/O事件、绘制事件、手势事件、接收其他Isolate消息等外部事件,Timer也是事件队列。

2、微任务队列(Microtask Queue)表示一个短时间内就会完成的异步任务。它的优先级最高,高于Event Queue,只要队列中还有任务,就可以一直霸占着事件循环。Microtask Queue添加的任务主要是由Dart内部产生,当然我们也可以自己添加任务到微任务队列中去,但是我们不要在Microtask Queue里面实现耗时操作避免阻塞Event Queue里的UI事件导致卡顿现象。因为微任务队列的优先级要比事件队列的高,所以事件循环每次循环总是先判断微任务队列中是否有任务需要执行,如果有则先执行微任务队列里的任务,执行完毕之后才会执行事件任务队列里的任务,就会造成卡顿。

      具体到两个队列的任务怎么创建添加我们后面再提,在了解了这两个队列之后我们再看看Dart的消息循环机制,下面这张图相信大家都见到过:
 

关于事件循环的,需要我们特别留意的:

1、在Microtask不为空的时候,Run next Microtask 之后回到最开始,首先判断的是是否还存在微任务,有的话还是优先处理的。

2、在Event不为空的时候,Run next event之后,还是会回去判断是否有Microtask,这点就把前面优先级的问题说的很明白了,这两点需要我们特别留意,在下面我们说完这两个对垒任务的添加之后,我们会写一个稍微比较复杂的方法,仔细的分析一下上面这个事件循环机制。

三:异步


在异步调用中有三个关键词 【async】【await】【Future】,其中async和await/Future是一起使用的,在Dart中可以通过async和await进行一个异步操作,async表示开始一个异步操作,也可以返回一个Future结果。如果没有返回值,则默认返回一个返回值为null的Future,这点也比较容易理解,就像下面的方法,返回值是Future,而我们不写返回return也是可以编译过去的,就是它默认自己返回一个返回值为null的Future。

Future handleMessage(String message) async {
print(message);
}

Future:默认的Future是异步运行的,也就是把任务放在Future函数体中,这个函数题会被异步执行。

async:异步函数标识,一般与await和Future配合使用。

await:等待异步结果返回,一般加在Future函数体之前,表明后面的代码要等这个Future函数体内的内容执行完在执行,实现同步执行。单独给函数添加async关键字, 没有意义,函数是否是异步的,主要看Future。

注意:Future<T>通过泛型指定类型的异步操作结果(不需要结果可以使用Future<void>)当一个返回Future对象的函数被调用时,函数将被放入队列等待执行并返回一个未完成的Future对象,等函数操作执行完成时,Future对象变为完成并携带一个值或一个错误。也就是说首先Future是个泛型类,可以指定类型。如果没有指定相应类型的话,则Future会在执行动态的推导类型。

       结合上面说的这几点,我们写个实际的小例子:
 
class asyncIsolate {

  Future<HttpClientRequest> dataReqeust() async {

    var httpClient = new HttpClient();
/// var uri = Uri.https('example.org', '/path', {'q': 'dart'});
/// print(uri); // https://example.org/path?q=dart
Future<HttpClientRequest> request =
httpClient.getUrl(Uri.https('jsonplaceholder.typicode.com', '/posts'));
return request;
} Future<String> loadData() async { HttpClientRequest request = await dataReqeust();
var response = await request.close();
var responseBody = await response.transform(Utf8Decoder()).join();
print('请求到的数据为:\n $responseBody');
return responseBody;
} }

上面的方法是一个请求数据的小demo,我们调用loadData方法进行数据请求,在运行到loadData内部时候,执行到await会阻塞async内部的执行,从而继续执行外面的代码,一直到dataReqeust的方法有返回,再接着async内部的执行,所以需要知道的事await不会阻塞方法外部代码的执行。

Future可以看做是一个延迟操作的封装,可以将异步任务封装为Future对象。获取到Future对象后,最简单的方法就是用await修饰,并等待返回结果继续向下执行。在Dart中,和时间相关的操作基本都和Future有关,例如延时操作、异步操作等,下面是一个最简单的延迟操作的例子:

/// 延迟操作
delayedWithFuture() { DateTime now = DateTime.now();
print("开始时间: $now");
Future.delayed(Duration(seconds: 10), () {
now = DateTime.now();
print("延迟10秒后的时间: $now");
}); /*
flutter: 开始时间: 2022-05-09 13:30:07.164114
flutter: 延迟10秒后的时间: 2022-05-09 13:30:17.171057
*/
}

Dart还支持对Future的链式调用,通过追加一个或多个then方法来实现,这个特性非常实用。例如一个延时操作完成后,会调用then方法,并且可以传递一个参数给then,比如下面的例子:

delayWithFutureThen() {

    Future.delayed(Duration(seconds: 5), () {
int age = 30;
return age;
}).then((onValue) {
onValue++;
print('我多大了啊 $onValue');
}); /*
flutter: 我多大了啊 31
*/
}

Future还有很多有意思的方法,比如 Future.doWhile() 、Future.any()、Future.wait(),我们简单的看一个,比如Future.wait的用法,假设我们有这个一个使用场景,等待在三个Future执行完之后我们还需要执行另外一个Future,这时候我们该怎么处理,下面的demo给了我们处理的方式,注意下输出的日志,我们第一个是延迟的Future,是延迟两秒后输出的。

///
void awaitWithFuture() async { Future future1 = Future.delayed(Duration(seconds: 2), () {
print(1);
return 1;
}); Future future2 = Future(() {
print(2);
return 2;
}); Future future3 = Future(() {
print(3);
return 3;
}); Future.wait([future1, future2, future3]).then((value) {
print(value);
}).catchError((error) {}); /*
flutter: 2
flutter: 3
flutter: 1
flutter: [1, 2, 3]
*/
}

微任务队列添加任务,我们通过scheduleMicrotask添加微任务,具体的我们就不在这再写了,的确添加比较简单,接下来我们写一个事件队列和微任务队列在一起的demo,我们梳理一下执行的过程,加深一下对事件循环的理解:

  analyseWithAsyncTask() {

    print('foundation start');
Future.delayed(Duration(seconds: 2), (() {
print('Future - delayed 2 second'); // --- 6
})); Future(() {
print('Future - 1'); // --- 2
}); Future(() {
print('Future - 2'); // --- 3
}); Timer(Duration(seconds: 3), (() {
print('Timer - delayed 3 second'); // --- 7
})); scheduleMicrotask((() {
print("Microtask - 1"); // --- 1
Future(() {
print('Future - 3'); // --- 5 【4的Future添加的比Future - 3要早】
});
})); Future(() {
scheduleMicrotask((() {
print("Microtask - 2"); // --- 4
}));
}); print('foundation end');
} /*
按照自己理解写出来的执行顺序 先到foundation start 再执行到
Future.delayed(Duration(seconds: 2), (() {
print('Future - delayed 2 second'); // --- 6
}));
没有这个Future添加到事件队列、后面的
Future(() {
print('Future - 2'); // --- 3
}); Timer(Duration(seconds: 3), (() {
print('Timer - delayed 3 second'); // --- 7
}));
也一样,没有先添加到事件队列、接下来是
scheduleMicrotask((() {
print("Microtask - 1"); // --- 1
Future(() {
print('Future - 3'); // --- 5 【4的Future添加的比Future - 3要早】
});
}));
判断微任务队列没有这个任务,添加到微任务队列。再到后面的
Future(() {
scheduleMicrotask((() {
print("Microtask - 2"); // --- 4
}));
});
也一样,也是没有就添加到事件队列,接着就是先打印foundation end
接下来判断有没有优先级更搞得微任务队列是否为空,判断有任务不为空,则执行微任务输出 - Microtask - 1 ,继续执行判断没有事件任务Future - 3
把事件任务添加到事件队列,注意这个事件任务的位置是在标记了// --- 4的事件后面的,执行完判断有没有微任务,发现没有了,开始添加的顺序执行事件任务
就输出了Future - 1 Future - 2 ,执行// --- 4的时候发现微任务,添加到微任务队列,执行下一个事件任务之前,判断有没有微任务,有的话就去执行微任务
就执行了Microtask - 2 ,继续判断微任务空了,继续事件任务。就到了Future - 3 最后两个延时的,安演示正长短 短的先执行 foundation start
foundation end
Microtask - 1
Future - 1
Future - 2
Microtask - 2
Future - 3
Future - delayed 2 second
Timer - delayed 3 second 实际日志输出:
flutter: foundation start
flutter: foundation end
flutter: Microtask - 1
flutter: Future - 1
flutter: Future - 2
flutter: Microtask - 2
flutter: Future - 3
flutter: Future - delayed 2 second
flutter: Timer - delayed 3 second
*/

四: 多线程 - Isolate


 

       Isolate是Dart平台对线程的实现方案,但和普通Thread不同的是,isolate拥有独立的内存,isolate由线程和独立内存构成。正是由于isolate线程之间的内存不共享,所以isolate线程之间并不存在资源抢夺的问题,所以也不需要锁。
 
      通过isolate可以很好的利用多核CPU,来进行大量耗时任务的处理。isolate线程之间的通信主要通过Port来进行,这个Port消息传递的过程是异步的。通过Dart源码也可以看出,实例化一个isolate的过程包括,实例化isolate结构体、在堆中分配线程内存、配置Port等过程。
 
  late SendPort subSendPort;

  /// 创建新的线程
dispatchQueueAsyncThread() async {
/// 主线程端口
ReceivePort mainThreadPort = ReceivePort(); /// 创建一个新的线程
/*
external static Future<Isolate> spawn<T>(
void entryPoint(T message), T message,
{bool paused = false, | { }里面的这些参数是可选类型的
bool errorsAreFatal = true, |
SendPort? onExit, |
SendPort? onError, |
@Since("2.3") String? debugName}); |
*/ Isolate isolate =
await Isolate.spawn<SendPort>(dataLoader, mainThreadPort.sendPort);
mainThreadPort.listen((message) {
///
print(message); if (message is SendPort) {
subSendPort = message;
print("子线程创建成功");
print("主线程收到了子线程的ReceivePort的Sendport了,可以通信了");
} else if (message is String) {
if (message == "closed") {
/// 结束这个线程
print('isolate kill');
isolate.kill();
}
}
});
} /// 主线程发送消息给子线程
mainSendMessageToSubThread() {
if (subSendPort != null) {
subSendPort.send("我是你的主线程");
}
} /// 主线程发送关闭端口的消息给子线程
/// 子线程关闭接口端口 并告诉主线程 主线程结束子线程
mainSendClosedThreadMessageToSubThread() {
if (subSendPort != null) {
subSendPort.send("close receiveport");
}
} // 这个SendPort是前面主线程的
static dataLoader(SendPort sendPort) async {
/// 子线程的ReceivePort创建
ReceivePort subThreadPort = ReceivePort(); /// 这是把子线程的ReceivePort的sendPort给了主线程 用于通信
sendPort.send(subThreadPort.sendPort); subThreadPort.listen((message) {
print("子线程收到的消息 $message");
if (message is String) {
/// 收到主线程让关闭接口的消息 就关闭 正确的话后面是在接受不到消息了
if (message == "close receiveport") {
sendPort.send('closed');
subThreadPort.close();
}
}
});
} /*
flutter: SendPort
flutter: 子线程创建成功
flutter: 主线程收到了子线程的ReceivePort的Sendport了,可以通信了
flutter: 子线程收到的消息 我是你的主线程
flutter: 子线程收到的消息 close receiveport
flutter: closed
flutter: isolate kill
*/

Isolate的线程更加偏向于底层,在生成一个Isolate之后,其内存是各自独立的,相互之间并不能进行访问,在进行Isolate消息传递的过程中,本质上就是进行Port的传递,通过上面的小例子我们基本上也就掌握了最基础的Flutter消息线程创建和线程之间的消息传递。

Flutter异步与线程详解的更多相关文章

  1. php为什么需要异步编程?php异步编程的详解(附示例)

    本篇文章给大家带来的内容是关于php为什么需要异步编程?php异步编程的详解(附示例),有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助. 我对 php 异步的知识还比较混乱,写这篇是为了 ...

  2. Javascript 异步加载详解

    Javascript 异步加载详解 本文总结一下浏览器在 javascript 的加载方式. 关键词:异步加载(async loading),延迟加载(lazy loading),延迟执行(lazy ...

  3. 通用线程:POSIX 线程详解,第 3 部分 条件互斥量(pthread_cond_t)

    使用条件变量提高效率 本文是 POSIX 线程三部曲系列的最后一部分,Daniel 将详细讨论如何使用条件变量.条件变量是 POSIX 线程结构,可以让您在遇到某些条件时“唤醒”线程.可以将它们看作是 ...

  4. “全栈2019”Java多线程第二十五章:生产者与消费者线程详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...

  5. Flutter完整开发实战详解

    Flutter完整开发实战详解(一.Dart语言和Flutter基础) Flutter完整开发实战详解(二. 快速开发实战篇) Flutter完整开发实战详解(三. 打包与填坑篇)

  6. python线程详解

    #线程状态 #线程同步(锁)#多线程的优势在于可以同时运行多个任务,至少感觉起来是这样,但是当线程需要共享数据时,可能存在数据不同步的问题. #threading模块#常用方法:'''threadin ...

  7. 通用线程:POSIX 线程详解,第 3 部分

    通用线程:POSIX 线程详解,第 3 部分 使用条件变量提高效率 Daniel Robbins, 总裁兼 CEO, Gentoo Technologies, Inc. 简介: 本文是 POSIX 线 ...

  8. POSIX 线程详解(经典必看)

    http://www.cnblogs.com/sunminmin/p/4479952.html 总共三部分: 第一部分:POSIX 线程详解                               ...

  9. mysql后台线程详解

    1.mysql后台线程 mysql后台线程主要用于维持服务器的正常运行和完成用户提交的任务,主要包括:master thread,read thread,write thread,redo log t ...

随机推荐

  1. Idea中使用Lombok 编译报找不到符号

    1.问题描述 项目中使用了lombok,但是在idea编译过程是出现找不到符号.报错如下图所示:   file @Data @ApiModel(value = "HeadTeacherVO& ...

  2. 简述 Memcached 内存管理机制原理?

    早期的 Memcached 内存管理方式是通过 malloc 的分配的内存,使用完后通过 free 来回收内存,这种方式容易产生内存碎片,并降低操作系统对内存的管理效 率.加重操作系统内存管理器的负担 ...

  3. Cloud Design Patterns & Architecture Styles

    Cloud Design Patterns Categories Data Management Design and Implementation Messaging Patterns Ambass ...

  4. 基于HTML5的网络拓扑图(1)

    什么是网络拓扑 网络拓扑,指构成网络的成员间特定的排列方式.分为物理的,即真实的.或者逻辑的,即虚拟的两种.如果两个网络的连接结构相同,我们就説它们的网络拓扑相同,尽管它们各自内部的物理接线.节点间距 ...

  5. EDM响应式邮件框架:MJML

    概述 新课题研究:响应式邮件框架MJML(MJML官网:https://mjml.io/)姐妹篇: EDM响应式邮件框架:Formerly Ink 介绍 MJML是一种标记语言,设计用于轻松实现一个响 ...

  6. APICloud案例源码、模块源码、考试源码、开发工具大集合!赶快收藏

    APICloud专注于APP开发定制技术,多年来不停为开发者奉献更多的资源.此次,APICloud将以往的的资源进行更新.整合,以合集的形式分享给广大的用户. APICloud应用案例源码合集 API ...

  7. canvas系列教程07-canvas动画基础1

    上面我们玩了一个图表,大家学好结构,然后在那个基础上去扩展各种图表,慢慢就可以形成自己的图表库了.也可以多看看一些国外的图表库简单的版本,分析分析,读代码对提高用处很大.我说了canvas两大主流用途 ...

  8. 王下邀月熊_Chevalier的前端每周清单系列文章索引

    感谢 王下邀月熊_Chevalier 分享的前端每周清单,为方便大家阅读,特整理一份索引. 王下邀月熊大大也于 2018 年 3 月 31 日整理了自己的前端每周清单系列,并以年/月为单位进行分类,具 ...

  9. java中的方法(method)到底怎么用?给个例子

    7.方法(method)   被调例子, int add(int x, int y){ return x+y; } 主调例子, for example: int result = add(5,3); ...

  10. Linux环境下Eclipse中快捷键不起作用

    在window->Preferences->general->keys中, 找到 content asist 修改下边值 Binding 改成 Alt+/ When 改为 Editi ...