Flutter 小技巧之面试题里有意思的异步问题

news2024/12/27 12:46:04

很久没更新小技巧系列了,本次简单介绍一下 Flutter 面试里我认为比较有意思的异步基础知识点。

首先我们简单看一段代码,如下代码所示,是一个循环定时器任务,这段代码里:

  • testFunc 循环每 1 秒执行一次 asyncWork
  • asyncWork 每次执行前判断 list 里是否还有数据
  • 如果存在数据,就做一些前置处理(延迟两秒模拟 do something)
  • 之后通过 removeAt 提取数据,完成输出
 List<String> list = ["1"];

  testFunc() {
    Timer.periodic(const Duration(seconds: 1), asyncWork);
  }

  asyncWork(t) async {
    if (list.isNotEmpty) {
      ///do something
      await Future.delayed(const Duration(seconds: 2));

      var item = list.removeAt(0);

      if (kDebugMode) {
        print("############ complete $item ############");
      }
    }
  }

那么上面这段代码有没有问题呢?实际上述代码在运行后是会出现报错,报错原因是 list 数组里是 empty ,但是我们调用了 removeAt(0)

这就很“奇怪”了,我们不是在 asyncWork 一开始就通过 isNotEmpty 判断了吗?为什么还会出现 removeAt(0) 的时候数组是空的情况?

这里就不得不说我们模拟 “do something” 时 await 的延迟 2s 的操作。

实际上对于 Timer.periodic 而言,他是固定以**「大概」** 1 秒的速度循环执行 asyncWork ,但是对于 asyncWork 而言,它需要等待 “do something” 操作,这里是固定两秒的时间,所以其实在 await 的时候, asyncWork 其实已经被从后面进入多次

所以虽然我们前面有 isNotEmpty 的判断,但是因为 “do something” 时 await 的延迟 2s 的操作,以至于最后执行 removeAt(0) 的时候,数组里的内容已经在一次被 remove 了,第二和第三次触发执行 removeAt 的时候,其实 list 里面已经没有数据了,所以会抛出异常。

为什么聊这个问题,因为这里是模拟问题执行,固定 2s 的情况很容易被推断出来问题,但是如果是在逻辑复杂的情况下,不同机器处理速度不一致的常见下,这种异步问题的定位就会变得“很模糊”

所以到这里你明白了为什么虽然前面做了 isNotEmpty 判断,但是后面 removeAt 还是会出现 RangeError(index) 的原因了吧。

那么有人可能会疑惑,Dart 里面不是单线程轮询的任务机制吗?为什么这里 await 之后,还会多次同时进入呢?

其实这里恰恰是因为 Dart 是单线程轮询的机制,所以才会出现这样多次进入的场景,所以我们要搞清楚, Timer 的定时机制实现, Timer 的底层定时能力是依赖于 isolate 实现的定时执行

我们知道 Flutter 里 Dart 是单线程轮询的机制,但是我们可以通过 isolate 去开启全新的隔离线程去实现真异步任务,而对于 Dart VM 来说,它是通过 isolate 的 port 机制实现的定时任务,所以在定时任务这里,他是通过 isolate 实现,然后通过 SendPort 去触发 callback 执行。

而在执行 callback 的时候,如下代码所示,callback(timer) 就是一次普通调用,它并没有 await 等操作,所以对于前面的 Timer.periodic 来说,它不管 asyncWork 是不是 Future ,也不管这个 async 是否已经执行完成,它只负责执行一下,然后进入下一次,所以最终造就了一开始代码里的逻辑判断出现问题:多次进入等待。

另外,还记得前面我们说过, Timer.periodic 是固定以「大概」1 秒的速度循环执行 asyncWork ,为什么这里用「大概」?因为 Timer.periodic 不是一个“可靠”的定时操作,在官方的注释里明确说明了:

The exact timing depends on the underlying timer implementation. No more than n callbacks will be made in duration * n time, but the time between two consecutive callbacks can be shorter and longer than duration.

其实原因也很简单,因为对于 VM 而言,定时器的操作是一个「批量处理」,不同运行环境下机器的处理能力存在差异,所以他最多保证 “duration * n 时间内最多会进行 n 次回调” ,但是无法保证 “两次连续回调之间的时间”,对于最终执行效果而言,这个时间可以短于或长于 duration 。

所以你将 Timer 的周期设置为 50 毫秒,但执行间隔时间可能会落在 40 -500 毫秒范围内,是的,有时候多个定时器可能会导致某次执行间隔“夸张”到 500 毫秒。

所以对于需要更精准的执行定时的任务,你可以选择使用:

  • Ticker ,因为对于 Flutter 来说,Flutter 会通过 ticker 每秒 60 帧的速度去渲染屏幕,所以可以将 Ticker 看作是一个特殊的周期计时器
  • 使用更短的 Timer.periodic ,例如开启一个全新的 isolate ,然后使用 microseconds:500 启动 Timer,之后在回调里自己判断 tickRate 去触发定时回调,reliable_periodic_timer 就是采用这种方式实现。

最后借助 Timer 这个例子,我们再聊聊 Flutter 里的异步模型,前面也提到说, Dart 默认都说单线程轮询机制,那他会是怎么样的一个轮询机制?

简单不严谨,但是好理解的解释:Dart 运行时 ,Root isolate 就是一个循环的线程,在执行 Dart 代码遇到 await Futureasync)的时候,Dart 事件循环可以先完成其他 Dart 代码,然后再 Future 完成后在返回执行原本下一步的代码

这就是上面 asyncWork 多次进入,每次进入都判断了 list.isNotEmpty , 因为 do something 需要 await 2 秒,所以都跳过了 list.removeAt ,导致最终执行 removeAt 的时候出现 RangeError(index) 的原因。

  asyncWork(t) async {
    if (list.isNotEmpty) {
      ///do something
      await Future.delayed(const Duration(seconds: 2));

      var item = list.removeAt(0);

      if (kDebugMode) {
        print("############ complete $item ############");
      }
    }
  }

但是在 Dart 里,异步等待也是分类型,简单可以分为微任务(MicroTask) 和 事件(Event) 两种

在 Dart 事件循环里首先会考虑微任务队列,如果微任务队列为空,才会转到事件队列中,这个机制主要是确保 MicroTask 异步操作优先于用户输入等事件。

对于 Flutter 而言:

  • MicroTask 是用来做一些及时和重要的内部操作,当时要保证 MicroTask 队列尽可能短
  • Event 是用于处理一些常规异步或者用户交互事件,例如当用户与应用交互时,可以创建一个点击事件并将其添加到 Event 队列中,Dart 事件循环去执行与点击事件相关的事件处理代码。

对于 MicroTask ,我们可让应用更精确地执行一些任务,而不会让 Event 队列负担过重导致 UI 不响应,例如用 MicroTask 来异步解析 json 数据到实体 object ,可以更好防止操作 UI 卡顿。

👆在不考虑新开 isolate 操作的情况下。

那么关于微任务和事件队列的关系,我们举个例子,将前面的 asyncWork 修改为如下代码所示,可以看到这里执行了一个 Future 和 一个 Future.microtask ,那么它的输出结果会是什么样?

asyncWork(t) async {
    print('starts');
    Future(() => print('This is a new Future'));
    Future.microtask(() => print('This is a micro task'));
    print('ends');
}

如下图所示,可以看到 microtask 虽然是后加入的,但是因为它是 MicroTask ,所以它会优选于 Future 执行,虽然 FutureFuture.microtask 是先后被执行,但是它们的 callback 触发存在优先级关系。

那么,再来一个升级本的,如下代码所示,可以看到现在是 Future 和 MicroTask 的各种嵌套异步:

asyncWork(t) async {
    print('main #1 of 2');
    scheduleMicrotask(() => print('microtask #1 of 3'));

    Future.delayed(Duration(seconds: 1), () => print('future #1 (delayed)'));

    Future(() => print('future #2 of 4'))
        .then((_) => print('future #2a'))
        .then((_) {
      print('future #2b');
      scheduleMicrotask(() => print('microtask #0 (from future #2b)'));
    }).then((_) => print('future #2c'));

    scheduleMicrotask(() => print('microtask #2 of 3'));

    Future(() => print('future #3 of 4'))
        .then((_) => Future(() => print('future #3a (a  future)')))
        .then((_) => print('future #3b'));

    Future(() => print('future #4 of 4'));
    scheduleMicrotask(() => print('microtask #3 of 3'));
    print('main #2 of 2');
  }

从结果我们可以看到:

  • main 的两个打印最先被执行,没毛病
  • 之后第一层的 3 个 Microtask 最先被执行,因为它们有 VIP
  • 然后第一层的 Future 开始被执行,首先被执行的是 future #2 of 4 和它后面打印,因为它们算一个 Future,这里有个有趣的地方,那就是穿插了一个 microtask #0 (from future #2b)'
  • 可以看到, microtask #0 (from future #2b)' 其实是在 Future 被执行完之后,才被执行,因为至少要保证一个 Future 完整执行
  • 之后剩下执行完第一层 Future 之后, delayed 被触发,然后执行完二层 Future,这里第二次 Future 因为它是 return 了一个 Future ,所以它会最后执行。

⚠️ 这里的 delayed 何时被执行并不是一定的,也可能是最后被执行。

通过上面的例子,可以看到整个异步以 MicroTask 为核心的运作,但是也需要保证 Future 执行完成之后再执行。

另外,而对于一些老版本的 Dart ,可能会存在需要第一层 Future 执行完成之后,才会触发 microtask #0 (from future #2b) 的情况。

最后,这里有个有趣的知识点,那就是其实 Future()factory 其实是通过 Timer 实现 ,包括 Future.delayed 也是一样,是不是很有趣,又回到了 Timer ,所以当有很多 Future.delayed 或者 Future() 构建的 callback 执行,其实本质上还是回归到了Timer 的「批量回调」。

所以用 Timer 为切入点去理解异步是一个很有意思的事情,特别一开始前面的例子,能很好的帮助去理解异步和 Timer 的工作机制,同时后面的 MicroTask 也可以细化大家对于 Flutter 里异步任务的认知,用来作为面试题也是一个很好的选择

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1835827.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

缓存技术实战[一文讲透!](Redis、Ecache等常用缓存原理介绍及实战)

目录 文章目录 目录缓存简介工作原理缓存分类1.按照技术层次分类2.按照应用场景分类3.按照缓存策略分类 应用场景1.硬件缓存2.软件缓存数据库缓存Web开发应用层缓存 3.分布式缓存4.微服务架构5.移动端应用6.大数据处理7.游戏开发 缓存优点缓存带来的问题 常见常用Java缓存技术1…

npm install cnpm -g 报错4048

npm install cnpm -g 报错4048 设置淘宝镜像&#xff1a; 报错如下&#xff1a; 其他博主提供的方法都尝试了&#xff0c;比如管理员权限打开终端&#xff0c;删除.npmrc文件&#xff0c;清除缓存npm cache clean -f等都试了无效&#xff0c;最后怀疑是npm和cnpm版本不对应&…

环境搭建---单机k8s

配置基础环境 关闭防火墙 [rootVM-20-14-centos ~]# systemctl stop firewalld && systemctl disable firewalld关闭selinux [rootVM-20-14-centos ~]# setenforce 0 && sed -i "s/SELINUXenforcing/SELINUXdisabled/g" /etc/selinux/config禁止s…

JVM性能优化工具及问题排查

jvm性能优化工具 jdk提供给我们了很实用的工具来分析JVM的状态&#xff0c;线程以及配置&#xff0c;这些工具包含于jdk中&#xff0c;并且以java实现&#xff0c;是JVM性能优化必不可少的工具集&#xff0c;这些工具都在$JAVA_HOME/bin下 jps、jinfo、jstack、jmap、jstat基本…

Java 开发实例:Spring Boot+AOP+注解+Redis防重复提交(防抖)

文章目录 1. 环境准备2. 引入依赖3. 配置Redis4. 创建防重复提交注解5. 实现AOP切面6. 创建示例Controller7. 测试8. 进一步优化8.1 自定义异常处理8.2 提升Redis的健壮性 9. 总结 &#x1f389;欢迎来到Java学习路线专栏~探索Java中的静态变量与实例变量 ☆* o(≧▽≦)o *☆嗨…

AI从云端到边缘:人员入侵检测算法的技术原理和视频监控方案应用

在当今数字化、智能化的时代&#xff0c;安全已成为社会发展的重要基石。特别是在一些关键领域&#xff0c;如公共安全、智能化监管以及智慧园区/社区管理等&#xff0c;确保安全无虞至关重要。而人员入侵检测AI算法作为一种先进的安全技术&#xff0c;正逐渐在这些领域发挥着不…

怎样打印微信文档文件?

在日常生活和工作中&#xff0c;我们经常需要打印微信中的文档文件&#xff0c;无论是工作资料、学习笔记还是其他重要信息。随着科技的发展&#xff0c;我们不再需要前往打印店进行繁琐的操作&#xff0c;而是可以通过一些便捷的在线打印平台轻松实现。今天&#xff0c;我们就…

git 快速将当前目录添加仓储

一、进入目录 git init git add . git commit -m "init" git remote add origin http://192.168.31.104/root/AutoBuildDemo.git 二、登录gitlab&#xff0c;创建项目AutoBuildDemo 最后执行&#xff1a; git push -u origin master

【C语言】--- 常见调试信息预处理器宏

在编程的艺术世界里&#xff0c;代码和灵感需要寻找到最佳的交融点&#xff0c;才能打造出令人为之惊叹的作品。而在这座秋知叶i博客的殿堂里&#xff0c;我们将共同追寻这种完美结合&#xff0c;为未来的世界留下属于我们的独特印记。 【C语言】--- 常见调试信息预处理器宏 开…

如何用Java SE数组实现高速的数字转换功能

哈喽&#xff0c;各位小伙伴们&#xff0c;你们好呀&#xff0c;我是喵手。运营社区&#xff1a;C站/掘金/腾讯云&#xff1b;欢迎大家常来逛逛 今天我要给大家分享一些自己日常学习到的一些知识点&#xff0c;并以文字的形式跟大家一起交流&#xff0c;互相学习&#xff0c;一…

技术分析:开源大模型的兴起与热门项目推荐

技术分析&#xff1a;开源大模型的兴起与热门项目推荐 引言 随着人工智能&#xff08;AI&#xff09;技术的不断发展&#xff0c;开源大模型成为了许多程序员和研究人员关注的焦点。开源项目不仅促进了技术的快速迭代和普及&#xff0c;还为更多的人提供了学习和实践的机会。…

推荐常用的三款源代码防泄密软件

三款源代码防泄密软件——安秉源代码加密、Virbox Protector 和 MapoLicensor——确实各自在源代码保护的不同方面有其专长。这些软件可以满足企业对于源代码保护的三大需求&#xff1a;防止泄露、防止反编译和防止破解。 安秉源代码加密&#xff1a; 专注于源代码文件的加密&…

惊艳视觉!7个让你大开眼界的数据可视化案例

数据可视化是指通过视觉呈现信息的一种方式&#xff0c;它仍处于不断演变的过程中。许多企业、政府和其他组织都使用数据可视化分析来寻求保持竞争优势。在界面设计中&#xff0c;数据可视化也呈现出越来越流行的趋势&#xff0c;学习数据可视化也是设计师保持竞争优势的一种方…

进程间通信以及线程的同步互斥机制

1.进程间通信机制 常用的六种通信机制&#xff1a; 管道、消息队列、共享内存、信号灯集、信号、Socket 管道&#xff08;Pipe&#xff09;和无名管道&#xff08;匿名管道&#xff09;&#xff1a; 管道是一种半双工的通信方式&#xff0c;数据只能单向流动&#xff0c;通常…

AI在医学中神奇应用

2022年11月30日&#xff0c;可能将成为一个改变人类历史的日子——美国人工智能开发机构OpenAI推出了聊天机器人ChatGPT-3.5&#xff0c;将人工智能的发展推向了一个新的高度。2023年11月7日&#xff0c;OpenAI首届开发者大会被称为“科技界的春晚”&#xff0c;吸引了全球广大…

前端菜鸡学习日记 -- 关于pnpm

哈咯哇大家&#xff0c;我又来了&#xff0c;最近稍微悠闲一些&#xff0c;所以就趁着这个机会学习一些新的知识&#xff0c;今天就是碰巧遇到了pnm&#xff0c;这个可以看作是npm的升级版本&#xff0c;比npm要快&#xff0c;用起来也更得劲更迅速 官网地址&#xff1a;https…

jupyter使用的一个奇怪bug——SyntaxError: invalid non-printable character U+00A0

bug来由&#xff1a;从其他部分例如kaggle里复制来的代码直接粘贴在jupyter notebook里&#xff0c;每一行代码都会出现&#xff1a; Cell In[5], line 1 warnings.filterwarnings(ignore) ^ SyntaxError: invalid non-printable character U00A0 单元格 In[5]&#xff0c;第 …

Rocky Linux 更换CN镜像地址

官方镜像列表&#xff0c;下拉查找 官方镜像列表&#xff1a;https://mirrors.rockylinux.org/mirrormanager/mirrorsCN 开头的站点。 一键更改镜像地址脚本 以下是更改从默认更改到阿里云地址 cat <<EOF>>/RackyLinux_Update_repo.sh #!/bin/bash # -*- codin…

react native中基于webview的腾讯图形验证码

react native中基于webview的腾讯图形验证码 效果实例图第三方库 腾讯验证码 效果实例图 第三方库 npm i react-native-webviewreact-native-webview import React, { useEffect, useState } from react; import { StyleSheet, Text, View } from react-native; import { We…

8.12 矢量图层面要素单一符号使用二(仅渲染中心点)

文章目录 前言仅渲染中心点&#xff08;Centroid fill&#xff09;QGis设置面符号为仅渲染中心点&#xff08;Centroid fill&#xff09;二次开发代码实现仅渲染中心点&#xff08;Centroid fill&#xff09; 总结 前言 本章介绍矢量图层线要素单一符号中仅渲染中心点&#xf…