所有分类
  • 所有分类
  • 游戏源码
  • 网站源码
  • 单机游戏
  • 游戏素材
  • 搭建教程
  • 精品工具

Dart多个Future队列完成顺序关系解析|原子性保障实战指南

Dart多个Future队列完成顺序关系解析|原子性保障实战指南 一

文章目录CloseOpen

Future队列的“顺序谜题”:为什么加入顺序≠完成顺序?

要搞懂Future的执行顺序,得先明白Dart的“事件循环”到底在干嘛。你可以把事件循环想象成餐厅里的服务员,他手里有两个任务清单:一个是“微任务队列”(VIP通道),一个是“事件队列”(普通通道)。服务员每次只会从一个队列里拿任务,而且必须把当前队列的任务全做完,才会换另一个队列。那Future任务在哪儿呢?普通的Future(比如用Future()创建的)会进事件队列,而像Future.microtask()创建的任务会进微任务队列。

这里有个关键:你把Future加进队列的顺序,是“入队顺序”;但服务员处理队列的顺序,是“优先级顺序”。比如你先加了3个事件队列的Future(任务A、B、C),又加了2个微任务队列的Future(任务D、E),服务员会先把D、E做完,再回头做A、B、C——这时候就算A是第一个入队的,也得等微任务队列空了才轮到它。这就是为什么有时候你觉得“先加的任务应该先执行”,结果却不是。

那具体到多个Future的执行顺序,常见的误区有三个:

误区一:“async/await写在前面,任务就先执行”

很多人以为用await就能控制顺序,比如这样写:

void main() async {

Future(() => print('任务1')).then((_) => print('任务1完成'));

await Future(() => print('任务2')).then((_) => print('任务2完成'));

Future(() => print('任务3')).then((_) => print('任务3完成'));

}

你猜执行结果是什么?实际会是:任务2→任务2完成→任务1→任务1完成→任务3→任务3完成。因为await会阻塞后面的代码,让“任务2”先执行,但“任务1”其实比“任务2”更早入队事件队列,只是被await的“任务2”插队了。我之前在写一个表单提交功能时就犯过这错,把日志输出的Future写在await前面,结果日志打印顺序全乱了,排查半天才发现是await的阻塞导致的。

误区二:“Future.wait能保证完成顺序和入参顺序一致”

Future.wait确实有这个特性——它接收一个Future列表,返回的结果列表顺序会和入参列表顺序一致,哪怕里面的Future实际完成顺序不一样。比如你传[futureA, futureB, futureC],就算futureC先完成,结果列表还是[aResult, bResult, cResult]。但你要注意:Future.wait是并发执行所有Future的,不是串行。我之前帮一个电商项目优化商品列表加载,他们用Future.forEach串行执行10个商品详情请求,加载要8秒,后来换成Future.wait并发执行,3秒就完事了,因为Future.wait会让所有任务同时跑,只是最后按入参顺序整理结果。

不过这里有个坑:如果某个Future报错,Future.wait会立刻抛出错误,其他没完成的Future也会被取消。所以如果你的场景需要“全部完成,哪怕部分失败”,得给每个Future加catchError,比如这样:

Future.wait([

fetchData1().catchError((e) => null),

fetchData2().catchError((e) => null),

]).then((results) { / 处理结果 / });

误区三:“队列里的任务一定按入队顺序执行”

这其实和事件循环的“插队规则”有关。比如你在一个Future里又创建了新的微任务或Future,就可能打乱顺序。举个例子:

void main() {

Future(() => print('任务A')); // 事件队列

Future.microtask(() => print('微任务1')); // 微任务队列

Future(() {

print('任务B');

Future.microtask(() => print('微任务2')); // 在任务B里创建微任务

});

}

执行顺序会是:微任务1→任务A→任务B→微任务2。因为任务A和任务B都在事件队列,按入队顺序执行,但任务B执行时创建的微任务2,会在当前事件队列任务(任务B)执行完后,优先于事件队列的其他任务(如果有的话)执行。这就像你在吃饭时突然接到一个VIP客户电话,得先接完电话再继续吃饭,哪怕后面还有其他普通客人在等。

为了让你更清楚不同顺序控制方法的区别,我整理了一个表格,你可以根据场景选:

控制方法 执行方式 结果顺序 适用场景
链式调用(then) 串行 严格按调用顺序 任务有依赖关系(如A的结果是B的参数)
Future.wait 并发 与入参顺序一致 任务无依赖,需要所有结果
Future.forEach 串行 按列表顺序 循环处理任务,需要前一个完成再开始下一个

(表格说明:以上方法的执行顺序基于Dart 3.0+版本测试,不同版本可能存在细微差异, 结合实际场景测试验证)

原子性保障:别让并发操作变成“数据混战”

解决了顺序问题,另一个头疼的就是“原子性”——简单说,就是一个操作要么完整执行完,要么完全不执行,不能被其他操作打断。在Dart异步编程里,最常见的原子性问题就是“竞态条件”:多个Future同时读写同一个变量,结果互相覆盖。比如你有一个计数器,两个Future同时读取到值为5,然后都加1,最后结果是6而不是7,这就是典型的竞态条件。

我之前维护一个Flutter聊天应用时就遇到过:用户快速点击“发送”按钮,两个发送请求的Future同时修改“未发送消息数”,结果导致未发送数显示错误,用户以为消息没发出去,又点了好几次,最后发了一堆重复消息。后来才发现,是因为两个Future同时操作了同一个状态变量,没有做原子性保护。

用“锁机制”控制资源访问

最直接的办法就是“加锁”——同一时间只允许一个Future操作共享资源。Dart里没有现成的锁类,但可以用bool变量模拟,比如这样:

bool _isLock = false; // 锁标记

Future updateCounter() async {

while (_isLock) {

await Future.delayed(Duration(milliseconds: 10)); // 锁被占用时等待

}

_isLock = true; // 加锁

try {

int current = _counter; // 读取

await Future.delayed(Duration(milliseconds: 50)); // 模拟耗时操作

_counter = current + 1; // 修改

} finally {

_isLock = false; // 释放锁

}

}

这个方法简单有效,但有个缺点:如果等待时间设置不合理,可能会浪费资源。我后来发现Dart的synchronized包(pub.dev上的第三方库)更好用,它提供了现成的锁机制,还支持超时设置,你可以直接用synchronized(() async { / 原子操作 / }),比自己写的锁更稳定。

用“Completer”实现串行化任务

如果你的场景是“多个任务必须按顺序执行,不能并行”,Completer是个好工具。Completer可以手动控制Future的完成时机,你可以把任务串成一个“流水线”,上一个任务完成后才开始下一个。比如处理用户连续点击的请求:

Completer? _taskCompleter;

Future handleClick() async {

if (_taskCompleter != null) {

await _taskCompleter!.future; // 如果有任务在执行,等待它完成

}

_taskCompleter = Completer();

try {

await performRequest(); // 执行实际操作

} finally {

_taskCompleter!.complete(); // 任务完成,释放Completer

_taskCompleter = null;

}

}

我在一个视频播放应用里用过这个方法:用户快速切换视频时,避免多个视频加载请求同时执行,用Completer确保上一个视频的停止和资源释放完成后,才开始下一个视频的加载,这样就不会出现音视频不同步的问题。

用“Isolate”隔离并发任务

如果你的操作非常耗时(比如大文件处理、复杂计算),直接在主线程用锁可能会阻塞UI,这时候可以用Dart的Isolate——它能创建独立的执行线程,和主线程互不干扰,通过消息传递数据,天然保证原子性(因为每个Isolate有自己的内存空间,不会共享变量)。

比如处理图片压缩:你可以把压缩任务放到Isolate里执行,主线程只负责发送图片数据和接收压缩结果,这样就算有多个压缩任务,也不会互相影响。不过Isolate之间的数据传递是“拷贝”而不是“共享”,所以不适合频繁传递大量数据,这点你要注意。

Dart官方文档里提到,Isolate更适合CPU密集型任务,而不是I/O密集型任务(比如网络请求),因为I/O操作本身会释放线程,用Isolate反而会增加开销。你可以根据任务类型选择:CPU密集型用Isolate,I/O密集型用锁或Completer就够了。

其实不管是顺序控制还是原子性保障,核心都是“理解Dart异步的底层逻辑”——事件循环怎么工作,任务队列怎么优先级,共享资源怎么保护。你不用死记硬背,多写几个小例子测试一下,比如故意写几个打乱顺序的Future,然后一步步调试,看看执行顺序到底怎么回事。我之前就是用这种“试错法”,把Future的各种API都玩了一遍,后来遇到异步问题,基本一眼就能看出是顺序问题还是原子性问题。

如果你按这些方法试了,遇到什么奇葩情况或者有更巧妙的技巧,欢迎在评论区告诉我,咱们一起把Dart异步这块硬骨头啃下来!


你用Isolate处理任务时,最容易踩的坑就是数据传递这块儿——Dart里Isolate之间不像线程那样共享内存,默认情况下,你往Isolate里传数据,不管是字符串、列表还是对象,都会走一遍“序列化→传输→反序列化”的流程,说白了就是把数据从头到尾复制一份,这就像你给朋友寄快递,得先把东西打包(序列化),朋友收到再拆包(反序列化),整个过程又慢又占空间。我之前在做一个图片编辑App的时候就吃过这亏,用Isolate处理2K分辨率的图片滤镜,每次传Uint8List数据,5MB的图片来回拷贝,手机直接卡顿半秒,用户体验差到不行,后来才发现是数据拷贝在拖后腿。

要解决这个问题,第一个办法就是用TransferableTypedData,这玩意儿专门给二进制数据开了“绿色通道”。你可以把它理解成“内存快递柜”——不用把数据整个打包,而是直接把内存里的地址告诉对方Isolate,对方直接去那个地址取数据,根本不用复制。比如处理图片、音频这种二进制流的时候,你把数据用TransferableTypedData.wrap()包一下再传,性能能提升一大截。我当时把图片数据换成这个方法后,同样5MB的图片,传递延迟从500毫秒降到了80毫秒,肉眼可见的流畅。不过要注意,这招只对二进制数据管用,像普通的Map、List这些对象还是得走常规拷贝,别搞错了场景。

再一个就是减少数据交互的“频率”,能一次传完的就别分好几次。很多人用Isolate的时候,喜欢把任务拆得太细,比如处理一个1000条数据的列表,先传100条过去处理,等结果回来再传下100条,这就相当于快递员跑10趟,每次只送一个小包裹,效率低得离谱。我后来学乖了,直接把整个列表和完整的处理逻辑(比如排序、过滤规则)打包扔给Isolate,让它自己在里面把所有数据处理完,最后只传一个处理好的结果回来,中间不发任何消息。就像你点外卖直接点套餐,而不是先点个主食再单点配菜,省了来回沟通的功夫。之前处理订单数据的时候试过,1000条数据分10次传要8秒,一次传完加处理总共才2秒,效率差太远了。所以你设计Isolate任务的时候,一定要想清楚:能不能把“中间过程”都放在Isolate内部完成?只在开始传必要的输入,结束传最终的输出?这样就能把数据拷贝的开销降到最低。


如何判断一个Future任务会进入微任务队列还是事件队列?

在Dart中,任务队列类型由创建方式决定:普通Future(如通过Future()、Future.delayed()创建)会进入事件队列;通过Future.microtask()创建的任务会进入微任务队列。 async/await中的异步任务默认会进入事件队列,除非显式使用Future.microtask()指定微任务队列。

使用Future.wait时,如果其中一个Future抛出错误,其他Future会继续执行吗?

默认情况下,当Future.wait中的某个Future抛出未捕获的错误时,Future.wait会立即终止并抛出该错误,其他未完成的Future不会被主动取消(仍会在后台执行),但它们的结果会被忽略。如果希望所有Future无论成功失败都执行完毕,可以为每个Future单独添加catchError(如future.catchError((e) => null)),确保错误被局部捕获。

事件循环中,微任务队列和事件队列的执行优先级是绝对的吗?有没有例外情况?

是的,优先级是绝对的:事件循环会先处理完微任务队列中的所有任务(直到队列为空),才会开始处理事件队列中的任务,没有例外情况。即使事件队列中有任务先入队,只要微任务队列不为空,就不会切换到事件队列执行。

在Dart中,除了锁机制和Completer,还有其他保障原子性的方法吗?

有两种常见方法可补充:一是使用dart:typed_data库中的Atomic类(如AtomicInteger、AtomicBool),适合简单数值类型的原子操作(如计数器增减),底层通过硬件级原子指令实现;二是利用Streams的单订阅特性,将异步任务封装为Stream,通过listen回调按顺序处理,确保前一个任务完成后再处理下一个数据,避免并发冲突。

使用Isolate处理任务时,如何避免因数据拷贝导致的性能问题?

Isolate间数据传递默认是深拷贝(通过序列化/反序列化实现),大量数据会影响性能。优化方案有两种:一是使用TransferableTypedData传递二进制数据(如图片、文件流),它会直接传递内存引用而非拷贝;二是减少数据交互频率,将复杂逻辑整体放入Isolate,仅传递必要的输入参数和最终结果,避免频繁通信。

原文链接:https://www.mayiym.com/43392.html,转载请注明出处。
0
显示验证码
没有账号?注册  忘记密码?

社交账号快速登录

微信扫一扫关注
如已关注,请回复“登录”二字获取验证码