【Flutter 异步编程 - 壹】 | 单线程下的异步模型

·  阅读 1615
【Flutter 异步编程 - 壹】 |  单线程下的异步模型

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

一、 本专栏图示概念规范

本专栏是对 异步编程 的系统探索,会通过各个方面去认知、思考 异步编程 的概念。期间会用到一些图片进行表达与示意,在一开始先对图中的元素基本概念 进行规范和说明。


1. 任务概念规范

任务 : 完成一项需求的基本单位。
分发任务: 触发任务开始的动作。
任务结束: 任务完成的标识。
任务生命期: 任务从开始到完成的时间跨度。

如下所示,方块 表示任务;当 箭头指向一个任务时,表示对该任务进行分发;任何被分发的任务都会结束。在任务分发和结束之间,有一条虚线进行连接,表示 任务生命期


2. 任务的状态

未完成 : Uncompleted
成功完成 : Completed with Success
异常结束 : Completed with Error

一个任务生命期间有三种状态,如下通过三种颜色表示。在 任务结束 之前,该任务都是 未完成 态,通过 浅蓝色 表示;任何被分发的任务都是为了完成某项需求,任何任务都会结束,在结束时刻根据是否完成需求,可分为 成功完成异常结束 两种状态,如下分别用 绿色红色 表示。


3. 时刻与时间线

机体 : 任务分发者或处理者。
时刻: 机体运行中的某一瞬间。
时间线: 所有时刻构成的连续有向轴线。

在一个机体运行的过程中,时间线是绝对的,通过 紫色有向线段 表示时间的流逝的方向。时刻 是时间线上任意一点 ,通过 黑点 表示。


4.同步与异步

同步 : 机体在时间线上,将任务按顺序依次分发。

同步执行任务时,前一个任务完成后,才会分发下一任务。意思是说: 任意时刻只有一个任务在生命期中。

异步: 机体在时间线上,在一个任务未完成时,分发另一任务。

也就是说通过异步编程,允许某时刻有两个及以上的任务在生命期中。如下所示,在 任务1 完成后,分发 任务2; 在 任务2 未结束的情况下,可以分发 任务 3 。此时对于任务 3 来说,任务 2 就是异步执行的。

image.png


二、理解单线程中的异步任务

上面对基本概念进行了规范,看起来可能比较抽象,下面我们通过一个小场景来理解一下。妈妈早上出门散步,临走前嘱咐:

小捷,别睡了。快起床,把碗刷一下,地扫一下。还有,没开水了,记得烧。

当前场景下只有小捷 一个机体,需要完成的任务有四个:起床刷碗拖地烧水


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 方法。日志输出如下:渲染任务的执行并不会因写入文件任务而阻塞,这就是异步处理的价值。

image.png


四、异步模型的延伸

1. 单线程异步模型的局限性

本文主要介绍 异步模型 的概念,认识异步的作用,以及 Dart 编程语言中异步方法的基本使用。至于代码中更具体的异步使用方式,将在后期文章中结合详细介绍。另外,一般情况下,Dart 是以 单线程 运行的,所以本文中强调的是 单线程 下的异步模型。

仔细思考一下,可以看出,单线程中实现异步是有局限性的。比如说需要解析一个很大的 json ,或者进行复杂的逻辑运算等 耗时任务,这种必须由 本机体 处理的逻辑,而不是 等待结果 的场景,是无法在单线程中异步处理的。

就像是 扫地刷碗 任务,对于单一机体来说,不可能同时参与到两个任务之中。在实际开发中这两个任务可类比为 解析超大 json显示解析中界面 两个任务。如果前者耗时三秒,由于单线程 中同步方法的阻塞,界面就会卡住三秒,这就是单线程异步模型的 局限性


2. 多线程与异步的关系

上面问题的本质矛盾是:一个机体无法 同时 参与到两件任务 具体执行过程中。解决方案也非常简单,一个人搞不定,就摇人呗。多个机体参与任务分配的场景,就是 多线程 。 很多人都会讨论 异步多线程 的关系,其实很简单:两个机体,一个 扫地,一个 刷碗,同一时刻,存在两个及以上的任务在生命期中,一定是异步的。毫无疑问,多线程异步模型 的一种实现方式。


3. Dart 中如何解决单线程异步模型的局限性

C++Java 这些语言有 多线程 的支持,通过 “摇人” 可以充分调度 CPU 核心,来处理一些计算密集型的任务,实现任务在时间上的最合理分配。

绝大多数人可能觉得 Dart 是一个单线程的编程语言,其实不然。可能是很多人并没有在 Flutter 端做过计算密集型的任务,没有对多线程迫切的需要。毕竟 移动/桌面客户端 大多是网络、数据库访问等 io 密集型 的任务,人手一个终端,没有什么高并发的场景。不像后端那样需要保证一个终端被百万人同时访问。

或者计算密集型的任务都有由平台机体进行处理,将结果通知给 Flutter 端。这导致 Dart 看起来更像是一个 任务分发者,发号施令的人,绝大多数时候并不需要亲自参与任务的执行过程中。而这正是单线程下的异步模型所擅长的:借他人之力,监听回调信息

其实我们在日常开发中,使用的平台相关的插件,其中的方法基本上都是异步的,本质上就是这个原因。平台 是个烧水壶,烧水任务只需要分发监听回调。至于水怎么烧开,是 平台 需要关心的,这和 网络 io磁盘 io 是很类似的,都是 请求响应 的模式。这种任务,由单线程的异步模型进行处理,是最有效的,毕竟 “摇人” 还是要管饭的。

那如果非要在 Dart 中处理计算密集型的任务,该如何是好呢?不用担心,Dartisolate 机制可以完成这项需求。关于这点,在后面会进行详述。认识 异步 是什么,是本文的核心,那本文就到这里,谢谢观看 ~

分类:
Android
收藏成功!
已添加到「」, 点击更改