一、 本专栏图示概念规范
1. 任务概念规范
2. 任务的状态
3. 时刻与时间线
4.同步与异步
二、理解单线程中的异步任务
1. 任务的分配
2.异步任务特点
3. 异步任务完成与回调
三、 Dart 语言中的异步
1.编程语言中与异步模型的对应关系
2.Dart 编程中的异步任务
3.当前任务分析
四、异步模型的延伸
1. 单线程异步模型的局限性
2. 多线程与异步的关系
3. Dart 中如何解决单线程异步模型的局限性
一、 本专栏图示概念规范本专栏是对 异步编程 的系统探索,会通过各个方面去认知、思考 异步编程 的概念。期间会用到一些图片进行表达与示意,在一开始先对图中的元素和 基本概念 进行规范和说明。
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 机制可以完成这项需求。关于这点,在后面会进行详述。认识 异步 是什么,是本文的核心,那本文就到这里,谢谢观看 ~
更多关于Flutter 单线程异步模型的资料请关注软件开发网其它相关文章!