【Flutter 异步编程 - 壹】 | 单线程下的异步模型
本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
一、 本专栏图示概念规范
本专栏是对 异步编程
的系统探索,会通过各个方面去认知、思考 异步编程
的概念。期间会用到一些图片进行表达与示意,在一开始先对图中的元素
和 基本概念
进行规范和说明。
1. 任务概念规范
任务
: 完成一项需求的基本单位。分发任务
: 触发任务开始的动作。任务结束
: 任务完成的标识。任务生命期
: 任务从开始到完成的时间跨度。
如下所示,方块
表示任务;当 箭头
指向一个任务时,表示对该任务进行分发
;任何被分发的任务都会结束。在任务分发和结束之间,有一条虚线
进行连接,表示 任务生命期
。
2. 任务的状态
未完成
: Uncompleted成功完成
: Completed with Success异常结束
: Completed with Error
一个任务生命期间有三种状态,如下通过三种颜色表示。在 任务结束
之前,该任务都是 未完成
态,通过 浅蓝色
表示;任何被分发的任务都是为了完成某项需求,任何任务都会结束,在结束时刻根据是否完成需求,可分为 成功完成
和 异常结束
两种状态,如下分别用 绿色
和 红色
表示。
3. 时刻与时间线
机体
: 任务分发者或处理者。时刻
: 机体运行中的某一瞬间。时间线
: 所有时刻构成的连续有向轴线。
在一个机体运行的过程中,时间线是绝对的,通过 紫色有向线段
表示时间的流逝的方向。时刻
是时间线上任意一点 ,通过 黑点
表示。
4.同步与异步
同步
: 机体在时间线上,将任务按顺序依次分发。
同步执行任务时,前一个任务完成后,才会分发下一任务。意思是说: 任意时刻只有一个任务在生命期中。
异步
: 机体在时间线上,在一个任务未完成时,分发另一任务。
也就是说通过异步编程,允许某时刻有两个及以上的任务在生命期中。如下所示,在 任务1
完成后,分发 任务2
; 在 任务2
未结束的情况下,可以分发 任务 3
。此时对于任务 3
来说,任务 2
就是异步执行的。
二、理解单线程中的异步任务
上面对基本概念进行了规范,看起来可能比较抽象,下面我们通过一个小场景来理解一下。妈妈早上出门散步,临走前嘱咐:
小捷,别睡了。快起床,把被子晒一下,地扫一下。还有,没开水了,记得烧。
当前场景下只有小捷
一个机体,需要完成的任务有四个:起床
、晒被
、拖地
、烧水
。
1. 任务的分配
当机体有多个任务需要分发时,需要对任务进行分配。认识任务之间的关系,是任务分配的第一步。只有理清关系,才能合理分配任务。分配过程中需要注意:
[1]
任务之间可能存在明确的先后顺序,比如起床
需要在 晒被
之前。[2]
任务之间先后顺序也可能无所谓,比如先扫地还是先晒被,并没有太大区别。[3]
某类任务只需要机体来分发,生命期中不需要机体处理,并且和后续的任务没有什么关联性,比如烧水任务。
像烧水这种任务,即耗时,又不需要机体在任务生命期中做什么事。如果这类任务使用同步处理,那么任务期间机体能做的事只有 等待
。对于一个机体来说,这种等待就会意味着阻塞,不能处理任何事。
结合日常生活,我们知道当前场景之中,想要发挥机体最大的效力,最好的方式是起床之后,先分发 烧水任务
,不需要等待烧水任务完成,就去执行晒被、扫地任务。这样的任务分配就是将 烧水
作为一个异步任务来执行的。
但在如果在分配时,将烧水作为最后一个任务,那么异步执行的价值就会消失。所以对任务的合理分配,对机体的处理效率是非常重要的。
2.异步任务特点
从上面可以看出,异步任务
有很明显的特征,并不是任何任务都有必要异步执行。特别是对于单一机体来说,任务生命期间需要机体亲自参与,是无法异步处理的。 比如一个人不能一边晒被
,一边 扫地
。所以对于单线程来说,像一些只需要 分发任务
,任务的具体执行逻辑由其他机体完成的任务,适合使用 异步
处理,来避免不必要的等待。
这种任务,在应用程序中最常见的是网络 io
和 磁盘 io
的任务。比如,从一个网络接口中获取数据,对于机体来说,只需要分发任务来发送请求,就像烧水时只需要装水按下启动键一样。而服务器如何根据请求,查询数据库来返回响应信息,数据如何在网络中传输的,和分发任务的机体没有关系。磁盘的访问也是一样,分发读写文件任务后,真正干活的是操作系统。
像这类任务通过异步处理,可以避免在分发任务后,机体因等待任务的结束而阻塞。在等待其他机体处理的过程中,去分发其他任务,可以更好地分配时间。比如下面所示,网络数据获取
的任务分发后,需要通过网络把请求传输给服务器,服务器进行处理,给出响应结果。
整个任务处理的过程,并不需要机体参与,所以分发 网络数据获取
任务后,无需等待任务完成,接着分发 构建加载中界面
的任务,来展示加载中的界面。从而给出用户交互的反馈,而不是阻塞在那里等待网络任务完成,这就是一个非常典型的异步任务使用场景。
3. 异步任务完成与回调
前面的介绍中可以看出,异步任务在分发之后,并不会等待任务完成,在任务生命期中,可以继续分发其他任务。但任何任务都会结束,很多时候我们需要知道异步任务何时完成
,以及任务的完成情况、任务返回的结果,以便该任务后续的处理。比如,在烧水完成之后,我们需要处理 冲水
的任务。
这就要涉及到一个对异步而言非常重要的概念:
回调
: 任务在生命期间向机体提供通知的方式。
比如 烧水
任务完成后,烧水壶 “叮”
的一声通知任务完成;或者烧水期间发生故障,发出报警提示。这种在任务生命期间向机体发送通知的方式称为回调
。在编程中,回调一般是通过 函数参数
来实现的,所以习惯称 回调函数
。 另外,函数可以传递数据,所以通过回调函数不仅可以知道任务结束的契机,还可以通过回调参数将任务的内部数据暴露给机体。
比如在实际开发中,分发 网络数据获取
的任务,其目的是为了通过网络接口获取数据。就像烧开水任务完成之后,需要把 开水
倒入瓶中一样。我们也需要知道 网络数据获取
的任务完成的时机,将获取的数据 "倒入"
界面中进行显示。
从发送异步任务,到异步任务结束的回调触发,就是一个异步任务完整的 生命期
。
三、 Dart 语言中的异步
上面只是介绍了 异步模型
中的概念,这些概念是共通的,无论什么编程语言都一样适用。就像现实中,无论使用哪国的语言表述,四则运算的概念都不会有任何区别。只是在表述过程中,表现形式会在语言的语法上有所差异。
1.编程语言中与异步模型的对应关系
每种语言的描述,都是对概念模型的具象化实现。这里既然是对 Flutter
中异步编程的介绍,自然要说一下 Dart
语言对异步模型的描述。
对于 任务
概念来说,在编程中和 函数
有着千丝万缕的联系:函数体
可以实现 任务处理的具体逻辑
,也可以触发 任务分发
的动作 。但我并不认为两者是等价的, 任务
有着明确的 目的性
,而 函数
是实现这种 目的
的手段。在编程活动中,函数
作为 任务
在代码中的逻辑体现,任务
应先于 函数
存在。
如下代码所示,在 main
函数中,触发 calculate
任务,计算 0 ~ count
累加值和计算耗时,并返回。其中 calculate
函数就是对该任务的代码实现:
void main(){
TaskResult result = calculate();
}
TaskResult calculate({int count = 10000000}){
int startTime = DateTime.now().millisecondsSinceEpoch;
int result = loopAdd(count);
int cost = DateTime.now().millisecondsSinceEpoch-startTime;
return TaskResult(
cost:cost,
data:result,
taskName: "calculate"
);
}
int loopAdd(int count) {
int sum = 0;
for (int i = 0; i <= count; i++) {
sum+=i;
}
return sum;
}
这里 TaskResult
类用于记录任务完成的信息:
class TaskResult {
final int cost;
final String taskName;
final dynamic data;
TaskResult({
required this.cost,
required this.data,
required this.taskName,
});
Map<String,dynamic> toJson()=>{
"taskName":taskName,
"cost":cost,
"data": data
};
}
2.Dart 编程中的异步任务
如下在计算之后,还有两个任务:saveToFile
任务,将运算结果保存到文件中;以及 render
任务将运算结果渲染到界面上。
void main() {
TaskResult result = cacaulate();
saveToFile(result);
render(result);
}
这里 render
任务暂时通过在控制台打印显示作为渲染,逻辑如下:
void render(TaskResult result) {
print("结果渲染: ${result.toJson()}");
}
下面是将结果写入文件的任务实现逻辑。其中 File
对象的 writeAsString
是一个异步方法,可以将内容写入到文件中。通过 then
方法设置回调,监听任务完成的时机。
void saveToFile(TaskResult result) {
String filePath = path.join(Directory.current.path, "out.json");
File file = File(filePath);
String content = json.encode(result);
file.writeAsString(content).then((File value){
print("写入文件成功:!${value.path}");
});
}
3.当前任务分析
如下是这三个任务的执行示意,在 saveToFile
中使用 writeAsString
方法将异步处理写入逻辑。
这样就像在烧水任务分发后,可以执行晒被一样。saveToFile
任务分发之后,不需要等待文件写入完成,可以继续执行 render
方法。日志输出如下:渲染任务的执行并不会因写入文件任务而阻塞,这就是异步处理的价值。
四、异步模型的延伸
1. 单线程异步模型的局限性
本文主要介绍 异步模型
的概念,认识异步的作用,以及 Dart
编程语言中异步方法的基本使用。至于代码中更具体的异步使用方式,将在后期文章中结合详细介绍。另外,一般情况下,Dart
是以 单线程
运行的,所以本文中强调的是 单线程
下的异步模型。
仔细思考一下,可以看出,单线程中实现异步是有局限性的。比如说需要解析一个很大的 json
,或者进行复杂的逻辑运算等 耗时任务
,这种必须由 本机体
处理的逻辑,而不是 等待结果
的场景,是无法在单线程中异步处理的。
就像是 扫地
和 晒被
任务,对于单一机体来说,不可能同时参与到两个任务之中。在实际开发中这两个任务可类比为 解析超大 json
和 显示解析中界面
两个任务。如果前者耗时三秒,由于单线程
中同步方法的阻塞,界面就会卡住三秒,这就是单线程异步模型的 局限性
。
2. 多线程与异步的关系
上面问题的本质矛盾是:一个机体无法 同时
参与到两件任务 具体执行过程中
。解决方案也非常简单,一个人搞不定,就摇人呗。多个机体参与任务分配的场景,就是 多线程
。
很多人都会讨论 异步
和 多线程
的关系,其实很简单:两个机体,一个 扫地
,一个 晒被
,同一时刻,存在两个及以上的任务在生命期中,一定是异步的。毫无疑问,多线程
是 异步模型
的一种实现方式。
3. Dart 中如何解决单线程异步模型的局限性
像 C++
、Java
这些语言有 多线程
的支持,通过 “摇人”
可以充分调度 CPU
核心,来处理一些计算密集型的任务,实现任务在时间上的最合理分配。
绝大多数人可能觉得 Dart
是一个单线程的编程语言,其实不然。可能是很多人并没有在 Flutter
端做过计算密集型的任务,没有对多线程迫切的需要。毕竟 移动/桌面客户端
大多是网络、数据库访问等 io 密集型
的任务,人手一个终端,没有什么高并发的场景。不像后端那样需要保证一个终端被百万人同时访问。
或者计算密集型的任务都有由平台机体
进行处理,将结果通知给 Flutter
端。这导致 Dart
看起来更像是一个 任务分发者
,发号施令的人,绝大多数时候并不需要亲自参与任务的执行过程中。而这正是单线程下的异步模型所擅长的:借他人之力,监听回调信息
。
其实我们在日常开发中,使用的平台相关的插件,其中的方法基本上都是异步的,本质上就是这个原因。平台
是个烧水壶,烧水任务只需要分发
和 监听回调
。至于水怎么烧开,是 平台
需要关心的,这和 网络 io
、磁盘 io
是很类似的,都是 请求
与 响应
的模式。这种任务,由单线程的异步模型进行处理,是最有效的,毕竟 “摇人”
还是要管饭的。
那如果非要在 Dart
中处理计算密集型的任务,该如何是好呢?不用担心,Dart
的 isolate
机制可以完成这项需求。关于这点,在后面会进行详述。认识 异步
是什么,是本文的核心,那本文就到这里,谢谢观看 ~
作者:张风捷特烈
链接:https://juejin.cn/post/7144878072641585166
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。