dart学习第 14 节:异步进阶 —— 异常处理与并发

60 阅读5分钟

上一节课课我们学习了异步编程的基础 ——Future 和 async/await,掌握了如何用同步风格编写异步代码。但在实际开发中,异步操作的异常处理、多个异步任务的协同以及性能优化同样重要。今天我们将深入探讨异步进阶内容,解决这些更复杂的问题。

一、异步异常捕获:try-catch + async 的深度实践

异步操作中的异常处理比同步代码更复杂,因为错误可能在 “未来某个时刻” 才会抛出。try-catch 结合 async/await 是处理异步异常的最佳实践,但需要注意一些细节。

1. 基本捕获方式

在 async 函数中,await 后面的代码如果抛出异常,会被 try-catch 捕获,就像同步代码一样:

Future<void> riskyOperation() async {
  await Future.delayed(Duration(seconds: 1));
  // 模拟异步操作中抛出异常
  throw Exception("异步操作失败:网络连接超时");
}

void main() async {
  try {
    print("开始执行风险操作");
    await riskyOperation(); // 等待异步操作,若抛出异常会被捕获
    print("操作成功完成(不会执行)");
  } catch (e) {
    // 捕获异步异常
    print("捕获到异常:${e.toString()}");
  } finally {
    // 无论成败都会执行
    print("操作结束,清理资源");
  }
}

// 输出:
// 开始执行风险操作
// 捕获到异常:Exception: 异步操作失败:网络连接超时
// 操作结束,清理资源

2. 嵌套异步调用的异常捕获

当 async 函数中包含多个异步调用时,try-catch 能捕获所有嵌套的异常:

Future<void> level3() async {
  await Future.delayed(Duration(milliseconds: 500));
  throw Exception("最内层错误"); // 抛出异常
}

Future<void> level2() async {
  await level3(); // 调用内层异步函数
}

Future<void> level1() async {
  await level2(); // 调用中层异步函数
}

void main() async {
  try {
    await level1(); // 调用外层异步函数
  } catch (e) {
    // 所有层级的异常都会被捕获
    print("顶层捕获到异常:$e"); // 输出:顶层捕获到异常:Exception: 最内层错误
  }
}

这体现了 async/await 的优势:异常传递方式与同步代码一致,避免了回调地狱中的异常处理嵌套。

3. 区分不同类型的异常

可以通过 on 类型 catch 捕获特定类型的异常,实现精细化处理:

// 自定义异常类型
class NetworkException implements Exception {
  final String message;
  NetworkException(this.message);
  @override
  String toString() => "NetworkException: $message";
}

class DatabaseException implements Exception {
  final String message;
  DatabaseException(this.message);
  @override
  String toString() => "DatabaseException: $message";
}

Future<void> fetchData() async {
  // 模拟随机异常
  await Future.delayed(Duration(seconds: 1));
  if (DateTime.now().second % 2 == 0) {
    throw NetworkException("服务器连接失败");
  } else {
    throw DatabaseException("数据查询错误");
  }
}

void main() async {
  try {
    await fetchData();
  } on NetworkException catch (e) {
    // 只捕获 NetworkException
    print("网络错误处理:$e");
    // 可以在这里执行重试逻辑
  } on DatabaseException catch (e) {
    // 只捕获 DatabaseException
    print("数据库错误处理:$e");
    // 可以在这里执行数据修复逻辑
  } catch (e) {
    // 捕获其他所有异常
    print("未知错误:$e");
  }
}

4. 未捕获异常的危险

如果异步异常未被捕获,会导致程序崩溃(在 Flutter 中会导 致应用闪退):

Future<void> unhandledError() async {
  throw Exception("未被捕获的异常");
}

void main() {
  unhandledError(); // 调用异步函数但未处理异常
  print("程序继续执行...");

  // 输出:
  // 程序继续执行...
  // (稍后)Unhandled exception: Exception: 未被捕获的异常
}

最佳实践:所有 await 调用都必须放在 try-catch 中,或通过 .catchError 处理异常。



二、Future 组合:协同多个异步任务

实际开发中经常需要处理多个异步任务,Dart 提供了 Future.waitFuture.any 等工具类方法,帮助我们灵活组合多个 Future

1. Future.wait:等待所有任务完成(且都成功)

当需要所有异步任务都完成后再继续(如并行加载多个资源),使用 Future.wait

// 模拟三个不同的异步任务
Future<String> fetchUser() =>
    Future.delayed(Duration(seconds: 1), () => "用户数据");
Future<String> fetchOrders() =>
    Future.delayed(Duration(seconds: 2), () => "订单数据");
Future<String> fetchProducts() =>
    Future.delayed(Duration(seconds: 1), () => "商品数据");

void main() async {
  print("开始并行加载数据...");

  // 等待所有 Future 完成(总耗时取决于最慢的任务,这里是 2 秒)
  List<String> results = await Future.wait([
    fetchUser(),
    fetchOrders(),
    fetchProducts(),
  ]);

  // 所有任务成功完成后才会执行
  print("所有数据加载完成:");
  print("用户:${results[0]}");
  print("订单:${results[1]}");
  print("商品:${results[2]}");
}

// 输出:
// 开始并行加载数据...
// (等待 2 秒)
// 所有数据加载完成:
// 用户:用户数据
// 订单:订单数据
// 商品:商品数据

注意Future.wait 中任何一个任务抛出异常,整个等待会立即失败,并抛出该异常:

Future<String> fetchSuccess() => Future.value("成功数据");
Future<String> fetchFailure() => Future.delayed(Duration(seconds: 1), () {
  throw Exception("某个任务失败");
});

void main() async {
  try {
    await Future.wait([fetchSuccess(), fetchFailure()]);
  } catch (e) {
    print("捕获到异常:$e"); // 输出:捕获到异常:Exception: 某个任务失败
  }
}

如果需要即使部分任务失败也要继续等待其他任务,可以为每个 Future 单独添加错误处理:

Future<String> fetchSuccess() => Future.value("成功数据");
Future<String> fetchFailure() => Future.delayed(Duration(seconds: 1), () {
  throw Exception("某个任务失败");
});

void main() async {
  // 为每个 Future 添加 catchError,将错误转换为正常值
  List<Future<String>> futures = [
    fetchSuccess(),
    fetchFailure().catchError((e) => "错误数据:${e.toString()}"),
  ];

  List<String> results = await Future.wait(futures);
  print(results); // 输出:[成功数据, 错误数据:Exception: 某个任务失败]
}

2. Future.any:等待第一个完成的任务

当需要多个任务中只要有一个完成就继续(如从多个镜像服务器下载同一资源,选最快的),使用 Future.any

// 模拟不同速度的异步任务
Future<String> fastTask() =>
    Future.delayed(Duration(seconds: 1), () => "快速任务结果");
Future<String> mediumTask() =>
    Future.delayed(Duration(seconds: 2), () => "中等任务结果");
Future<String> slowTask() =>
    Future.delayed(Duration(seconds: 3), () => "慢速任务结果");

void main() async {
  print("等待第一个完成的任务...");

  // 只等待第一个完成的任务(这里是 1 秒后完成的 fastTask)
  String firstResult = await Future.any([fastTask(), mediumTask(), slowTask()]);

  print("第一个完成的任务结果:$firstResult"); // 输出:第一个完成的任务结果:快速任务结果
}

注意Future.any 中第一个失败的任务会导致整体失败,除非该任务已被单独处理错误:

Future<String> fastFailure() => Future.delayed(Duration(seconds: 1), () {
  throw Exception("快速失败");
});
Future<String> slowSuccess() =>
    Future.delayed(Duration(seconds: 2), () => "慢速成功");

void main() async {
  try {
    // 第一个完成的是 fastFailure,会导致整体失败
    await Future.any([fastFailure(), slowSuccess()]);
  } catch (e) {
    print("捕获到异常:$e"); // 输出:捕获到异常:Exception: 快速失败
  }

  // 正确处理:为可能先失败的任务添加错误处理
  String result = await Future.any([
    fastFailure().catchError((e) => "错误"), // 错误时返回 null
    slowSuccess(),
  ]);
  print("最终结果:$result"); // 输出:最终结果:错误
}

3. Future.forEach:按顺序处理可迭代对象

Future.forEach 会按顺序执行异步操作(完成一个再执行下一个),适合需要串行处理的场景:

void main() async {
  List<int> ids = [1, 2, 3];

  // 按顺序处理每个 id,每个操作完成后再处理下一个
  await Future.forEach(ids, (int id) async {
    await Future.delayed(Duration(seconds: 1)); // 模拟耗时操作
    print("处理完 id: $id");
  });

  print("所有 id 处理完成");
}

// 输出(每隔 1 秒输出一行):
// 处理完 id: 1
// 处理完 id: 2
// 处理完 id: 3
// 所有 id 处理完成


三、异步循环与性能陷阱:避免循环中创建 Future

在循环中处理异步操作时,很容易陷入性能陷阱。错误的写法会导致资源浪费或执行效率低下。

1. 错误写法:循环中创建大量并行 Future

// 模拟单个异步任务
Future<void> processItem(int i) async {
  await Future.delayed(Duration(milliseconds: 100));
  // print("处理完第 $i 项");
}

void main() async {
  final stopwatch = Stopwatch()..start();

  // 错误:一次性创建 1000 个并行的 Future
  List<Future> futures = [];
  for (int i = 0; i < 1000; i++) {
    futures.add(processItem(i));
  }
  await Future.wait(futures);

  stopwatch.stop();
  print("总耗时:${stopwatch.elapsedMilliseconds} 毫秒"); // 约 100-200 毫秒(并行优势)
}

表面看并行处理很快,但当循环次数很大(如 10 万次)时:

  • 会瞬间创建大量 Future 对象,消耗大量内存
  • 可能触发系统对并发连接的限制(如网络请求)
  • 导致 CPU 或 I/O 资源竞争,反而降低效率

2. 改进写法:控制并发数量

通过分批处理限制并发数,避免资源耗尽:

// 模拟单个异步任务
Future<void> processItem(int i) async {
  await Future.delayed(Duration(milliseconds: 100));
  // print("处理完第 $i 项");
}

// 分批处理:每次处理 10 个,处理完一批再处理下一批
Future<void> processInBatches(List<int> items, int batchSize) async {
  for (int i = 0; i < items.length; i += batchSize) {
    int end = i + batchSize;
    List<int> batch = items.sublist(i, end > items.length ? items.length : end);

    // 并行处理当前批次
    await Future.wait(batch.map((item) => processItem(item)));
    print("完成第 ${i ~/ batchSize + 1} 批处理");
  }
}

void main() async {
  final stopwatch = Stopwatch()..start();

  // 创建 1000 个待处理项
  List<int> items = List.generate(1000, (index) => index);

  // 每批处理 10 个
  await processInBatches(items, 10);

  stopwatch.stop();
  print("总耗时:${stopwatch.elapsedMilliseconds} 毫秒"); // 约 1000 毫秒(10 批 × 100 毫秒)
}

虽然总耗时增加,但避免了资源竞争和系统限制,适合大规模处理场景。

3. 正确的串行循环:用 await 控制顺序

如果任务必须串行执行(后一个依赖前一个的结果),应在循环内部使用 await

// 模拟单个异步任务
Future<void> processItem(int i) async {
  await Future.delayed(Duration(milliseconds: 100));
  // print("处理完第 $i 项");
}

void main() async {
  final stopwatch = Stopwatch()..start();

  // 正确:串行执行,完成一个再开始下一个
  for (int i = 0; i < 5; i++) {
    await processItem(i); // 每次循环等待当前任务完成
  }

  stopwatch.stop();
  print("总耗时:${stopwatch.elapsedMilliseconds} 毫秒"); // 约 500 毫秒(5 × 100 毫秒)
}

注意:不要在循环外收集 Future 再 wait,这会变成并行执行:

// 模拟单个异步任务
Future<void> processItem(int i) async {
  await Future.delayed(Duration(milliseconds: 100));
  // print("处理完第 $i 项");
}

// 错误:看似串行,实际并行
void main() async {
  List<Future> futures = [];
  for (int i = 0; i < 5; i++) {
    futures.add(processItem(i)); // 立即创建所有 Future
  }
  await Future.wait(futures); // 并行执行,总耗时 ~100 毫秒
}


四、异步代码的调试技巧

异步代码的调试比同步代码更复杂,因为执行顺序不直观。以下是实用技巧:

  1. 使用 print 或日志工具:在关键节点打印时间戳和变量状态,跟踪执行顺序。
  2. 利用 async 函数的栈跟踪:Dart 会记录异步操作的调用栈,异常时能显示完整的调用路径:
Future<void> a() async => await b();
Future<void> b() async => await c();
Future<void> c() async => throw Exception("错误发生");

void main() async {
  try {
    await a();
  } catch (e, stackTrace) {
    print("异常:$e");
    print("栈跟踪:$stackTrace"); // 会显示从 c → b → a → main 的完整路径
  }
}
  1. 在 Flutter DevTools 中调试:Flutter 提供的 DevTools 有专门的异步任务跟踪面板,可直观查看所有活跃的 Future