Dart如何检测未完成Future及泄漏资源解决程序挂起问题
Dart CLI应用挂起资源泄漏检测方案
开发调用第三方库的Dart控制台应用时,第三方方法执行返回后CLI会挂起约10秒才退出,根因是第三方库创建的资源未正常关闭、异步任务未执行完成。目前已通过runZonedGuarded配合ZoneSpecification实现了未取消定时器的检测与自动取消,简单场景下验证有效,但覆盖场景不全,需要补充未完成Future、其他类型资源泄漏的检测能力。
已实现的定时器检测试验代码
#! /usr/bin/env dcli import 'dart:async'; import 'package:dcli/dcli.dart'; import 'package:onepub/src/pub/global_packages.dart'; import 'package:onepub/src/pub/system_cache.dart'; import 'package:onepub/src/version/version.g.dart'; import 'package:pub_semver/pub_semver.dart'; void main(List<String> arguments) async { print(orange('OnePub version: $packageVersion ')); print(''); print(globals); // await globals.repairActivatedPackages(); await runZonedGuarded(() async { Timer(Duration(seconds: 20), () => print('timer done')); unawaited(Future.delayed(Duration(seconds: 20))); var completer = Completer(); unawaited( Future.delayed(Duration(seconds: 20), () => completer.complete())); // await globals.activateHosted( // 'dcli_unit_tester', // VersionConstraint.any, // null, // all executables // overwriteBinStubs: true, // url: null, // hostedUrl, // ); print('end activate'); }, (error, stackTrace) { print('Uncaught error: $error'); }, zoneSpecification: buildZoneSpec()); print('end'); checkLeaks(); // await entrypoint(arguments, CommandSet.ONEPUB, 'onepub'); } late final SystemCache cache = SystemCache(isOffline: false); GlobalPackages? _globals; GlobalPackages get globals => _globals ??= GlobalPackages(cache); List<void Function()> actions = []; List<Source<Timer>> timers = []; int testCounter = 0; int timerCount = 0; int periodicCallbacksCount = 0; int microtasksCount = 0; ZoneSpecification buildZoneSpec() { return ZoneSpecification( createTimer: (source, parent, zone, duration, f) { timerCount += 1; final result = parent.createTimer(zone, duration, f); timers.add(Source(result)); return result; }, createPeriodicTimer: (source, parent, zone, period, f) { periodicCallbacksCount += 1; final result = parent.createPeriodicTimer(zone, period, f); timers.add(Source(result)); return result; }, scheduleMicrotask: (source, parent, zone, f) { microtasksCount += 1; actions.add(f); final result = parent.scheduleMicrotask(zone, f); return result; }, ); } void checkLeaks() { print(actions.length); print(timers.length); print('testCounter $testCounter'); print('timerCount $timerCount'); print('periodicCallbacksCount $periodicCallbacksCount'); print('microtasksCount $microtasksCount'); for (var timer in timers) { if (timer.source.isActive) { print('Active Timer: ${timer.st}'); timer.source.cancel(); } } } class Source<T> { Source(this.source) { st = StackTrace.current; } T source; late StackTrace st; }
已验证的问题场景
实际测试中捕获到HTTP连接空闲保活定时器导致的挂起,对应堆栈如下,印证了HTTP连接未正确关闭的问题:
Active Timer: #0 new Source (file:///home/bsutton/git/onepub/onepub/bin/onepub.dart:105:21) #1 buildZoneSpec.<anonymous closure> (file:///home/bsutton/git/onepub/onepub/bin/onepub.dart:68:18) #2 _CustomZone.createTimer (dart:async/zone.dart:1388:19) #3 new Timer (dart:async/timer.dart:54:10) #4 _HttpClientConnection.startTimer (dart:_http/http_impl.dart:2320:18) #5 _ConnectionTarget.returnConnection (dart:_http/http_impl.dart:2381:16) #6 _HttpClient._returnConnection (dart:_http/http_impl.dart:2800:41) #7 _HttpClientConnection.send.<anonymous closure>.<anonymous closure>.<anonymous closure> (dart:_http/http_impl.dart:2171:25) #8 _rootRunUnary (dart:async/zone.dart:1434:47)
分类型泄漏检测实现方案
1. 未完成Future检测
Dart没有提供直接枚举所有未完成Future的内置API,必须通过Zone层拦截包装实现追踪
- 在
ZoneSpecification中重写registerCallback、registerUnaryCallback、registerBinaryCallback三个回调注册入口,给所有Zone内注册的Future回调绑定创建堆栈、分配唯一追踪ID - 给追踪Zone内创建的Future统一注册完成回调,Future正常执行完成/报错时标记为已结束,泄漏检查阶段输出所有未标记完成的Future的创建堆栈
- 针对
Completer实例做额外拦截:包装completer.complete、completer.completeError方法,追踪未触发完成的Completer——绝大多数长期挂起的Future本质都是未执行complete的Completer实例
2. 其他常见资源泄漏检测
- HTTP客户端类资源:除了已捕获的连接保活定时器,可在Zone中拦截
HttpClient的创建逻辑,统一维护当前Zone内创建的客户端实例列表,执行泄漏检查时遍历调用close(force: true)强制关闭,避免空闲连接保活逻辑阻塞程序退出 - 文件句柄/网络IO资源:拦截
File.open、File.openRead、File.openWrite、Socket、RawSocket、RawDatagramSocket等IO资源的创建方法,追踪未调用close()的活跃句柄,退出阶段统一释放 - Isolate泄漏:拦截
Isolate.spawn、Isolate.spawnUri的创建逻辑,记录所有派生的子Isolate,退出检查阶段遍历调用kill(priority: Isolate.immediate)强制终止,避免子Isolate事件循环存活阻塞主Isolate退出 - 流订阅泄漏:拦截
Stream.listen调用,记录当前活跃的StreamSubscription实例,退出阶段统一调用cancel()取消订阅,避免流持有资源不释放
3. 快速退出兜底方案
如果不需要保证所有资源优雅释放,业务逻辑执行完成后可直接调用exit(0)立即终止进程,跳过Dart VM等待所有异步资源清空的逻辑。注意该方法会直接终止进程,不会执行任何未完成的异步回调、IO flush操作,需根据业务场景选择使用。
内容的提问来源于stack exchange,提问作者Brett Sutton
相关产品推荐
相关产品推荐

