异步支持
async
Dart异步基础: 事件循环与单线程模型
理论
在深入Dart的异步特性之前,理解其底层的运行机制至关重要。Dart,作为一种单线程语言,在处理耗时操作时,通过事件循环(Event Loop)实现非阻塞(non-blocking)的并发。这意味着Dart代码一次只能执行一个任务,但这并不意味着它不能同时处理多个任务。
单线程模型(Single Thread Model): Dart应用程序在执行时,只有一个主线程(main thread)负责执行代码。这意味着所有UI更新、变量计算等操作都在这一个线程上进行。这种模型简化了并发编程,因为它消除了传统多线程编程中常见的锁(locks)、死锁(deadlocks)等复杂问题。
事件循环(Event Loop): 事件循环是Dart应用程序的“心跳”,它不断地检查是否有待处理的任务。当主线程空闲时,事件循环就会从两个队列中取出任务并执行:
- 事件队列(Event Queue)/消息队列(Message Queue): 这里存放着来自外部事件(如用户点击、定时器触发、网络响应、文件I/O完成)的任务。这些任务通常是耗时的、非阻塞的。当一个异步操作完成时,它的回调函数(callback function)会被放入事件队列。
- 微任务队列(Microtask Queue): 微任务队列的优先级高于事件队列。它主要用于处理那些需要尽快执行但又不能立即执行的任务,例如
Future.then()或者scheduleMicrotask。在事件循环从事件队列中取任务之前,它会先清空微任务队列中的所有任务。
工作流程概览:
- Dart程序启动,主线程开始执行
main()函数。 main()函数执行完毕后,主线程进入空闲状态。- 事件循环启动,它会: a. 检查微任务队列。如果微任务队列中有任务,就将其全部取出并执行,直到队列为空。 b. 检查事件队列。如果事件队列中有任务,就取出一个并执行。 c. 重复步骤 a 和 b。
- 当两个队列都为空时,程序退出。
现实开发场景类比: 想象一下你是一个咖啡师(主线程),你正在一家繁忙的咖啡店(Dart应用程序)工作。
- 制作咖啡(同步操作): 你手边的工作,你必须立即完成一个订单才能开始下一个。
- 顾客排队(事件队列): 有些顾客点了一杯复杂的咖啡,需要一些时间制作。你不能一直等着他们,所以你记录下他们的订单,告诉他们稍后回来取。当咖啡机准备好一杯咖啡时(异步操作完成),你会把这个“咖啡制作完成”的消息放在一个“待处理订单”列表里(事件队列)。
- 快速清理(微任务队列): 在等待咖啡机完成冲泡的同时,你发现柜台有些脏(一个微任务)。你会优先把柜台擦干净,因为这不占用主要的制作咖啡时间,而且需要尽快解决。只有当所有的清理工作都完成后,你才会去查看“待处理订单”列表。
这个比喻说明了,虽然你只有一个咖啡师(一个线程),但通过合理地安排任务和利用后台设备(异步操作),你可以高效地处理大量订单而不会阻塞。
示例
import 'dart:async'; // 导入异步库,用于scheduleMicrotask
void main() {
print('1. main函数开始');
// 立即执行的同步代码
print('2. 同步任务执行');
// 异步任务一:Future,会被放入事件队列
Future(() => print('4. Future任务执行 (事件队列)'));
// 异步任务二:Future.delayed,也会被放入事件队列 (延迟后)
Future.delayed(Duration.zero, () => print('5. Future.delayed(Duration.zero)任务执行 (事件队列,但可能在4后)'));
// 异步任务三:scheduleMicrotask,会被放入微任务队列
scheduleMicrotask(() => print('3. Microtask任务执行 (微任务队列)'));
print('6. main函数结束'); // main函数结束,主线程将空闲,事件循环开始工作
}
/*
预期输出 (理论上,实际情况可能因VM实现略有微小差异,但大体顺序不变):
1. main函数开始
2. 同步任务执行
6. main函数结束
3. Microtask任务执行 (微任务队列)
4. Future任务执行 (事件队列)
5. Future.delayed(Duration.zero)任务执行 (事件队列,但可能在4后)
*/
运行结果分析:
'1. main函数开始'和'2. 同步任务执行'立即打印,因为它们是同步代码。scheduleMicrotask将其回调放入微任务队列。Future和Future.delayed将其回调放入事件队列。'6. main函数结束'打印。此时main()函数执行完毕,主线程空闲。- 事件循环开始工作。它首先清空微任务队列,所以
'3. Microtask任务执行 (微任务队列)'被打印。 - 微任务队列为空后,事件循环开始从事件队列中取任务。因此
'4. Future任务执行 (事件队列)'和'5. Future.delayed(Duration.zero)任务执行 (事件队列)'被打印。Future.delayed(Duration.zero)并不是立即执行,它只是将其回调函数安排在至少一个tick(一次事件循环迭代)后执行,所以它仍然在事件队列中。
注意: Future.delayed(Duration.zero)并不意味着“立即执行”,它意味着“在下一个可能的事件循环迭代中执行”,这与直接Future(() => ...)的行为非常相似,两者都会进入事件队列。
注意事项 (Notes)
- 耗时同步操作的风险(Blocking Main Thread):
注意: 在Dart的单线程模型中,执行任何耗时的同步操作都会阻塞主线程,导致UI卡顿(在Flutter应用中)或应用程序无响应。例如,一个大数据量的计算循环、一个同步的网络请求等。
void blockingFunction() {
print('开始耗时同步计算...');
// 模拟一个非常耗时的同步操作
var sum = 0;
for (int i = 0; i < 1000000000; i++) {
sum += i;
}
print('耗时同步计算完成, sum: $sum');
}
void main() {
print('主线程开始');
// 这将阻塞主线程,直到blockingFunction执行完毕
blockingFunction();
print('主线程结束'); // 只有blockingFunction完成后才会打印
}
为了避免阻塞主线程,所有耗时操作都应该通过异步方式处理。
-
Duration.zero的误解:
注意:Future.delayed(Duration.zero, () => ...)并不等同于同步执行。它只是将任务安排在事件队列中,等待事件循环在可能的下一个时刻执行。它保证了任务不会立即执行,而是给当前正在执行的代码一个完成的机会。 -
微任务与事件任务的场景选择:
注意: 微任务用于需要尽快完成且不被打断的异步逻辑,例如在UI渲染或后续的事件处理之前完成一些内部状态更新。然而,过度使用微任务队列可能会延迟事件队列中的任务处理,因为它会优先清空微任务队列。一般情况下,Future(事件队列)是更常用的异步并发处理方式。只有在特定需要更高优先级的异步处理时,才考虑scheduleMicrotask。
Future: 处理单个异步结果
理论
Future是Dart中表示一个异步操作结果的对象。它代表一个尚未完成但预计未来会完成(或失败)的计算。当一个异步操作启动时,它会立即返回一个Future对象。这个Future最终会以两种状态之一完成:
- 成功(complete with a value):
Future对象携带一个值完成。 - 失败(complete with an error):
Future对象携带一个错误或异常完成。
Future对象本身是一个占位符,它承诺在未来的某个时间点提供结果。你可以通过注册回调函数来处理Future完成时的结果。
Future的生命周期:
- 未完成(uncompleted):
Future刚刚创建,异步操作还在进行中。 - 已完成(completed): 异步操作已经结束,
Future要么带值成功,要么带错误失败。
核心方法:
-
then(void onValue(T value), {Function onError}):
这是最常用的方法,用于注册当Future成功完成时的回调函数onValue。onError是可选的,用于处理Future失败的情况。then()方法返回一个新的Future,这使得Future可以进行链式调用,将多个异步操作串联起来。 -
catchError(Function onError, {bool test(Object error)}):
专门用于处理Future链中出现的错误。它会捕获到链条中上一个Future抛出的错误,并可以根据test函数进行筛选。catchError()也会返回一个新的Future,如果错误被成功处理,这个新的Future会以正常值完成,否则会继续向下传递错误。 -
whenComplete(void action()):
注册一个回调函数,无论Future是成功还是失败,都会在Future完成时执行。通常用于清理资源。whenComplete()也会返回一个新的Future,它会继续传递之前Future的完成结果(成功或失败)。
现实开发场景类比: 想象你点了一份外卖(异步操作)。
- 下单成功: 你立即收到一个订单号(一个
Future对象)。你不知道外卖什么时候送到,但你知道它最终会送达。 - 等待外卖: 你不需要一直盯着手机(阻塞主线程),你可以做其他事情。
- 外卖送达:
- 成功送达(
onValue): 外卖员将餐食交给你,你就可以吃了。 - 送错地址/食物坏了(失败
onError): 你收到一个错误,需要联系客服。
- 成功送达(
- 无论外卖是否成功(
whenComplete): 你都会去厨房准备碗筷,或者整理桌面。 - 连续操作(
then()链式调用): 收到外卖 -> 吃掉外卖 -> 洗碗。每一步都是在前一步完成后才能进行。
示例
import 'dart:async'; // 导入async库
// 模拟一个网络请求,返回一个Future<String>
Future<String> fetchUserData(String userId) {
// 使用Future.delayed模拟网络延迟
return Future.delayed(Duration(seconds: 2), () {
if (userId == 'user123') {
return 'User Data for $userId: Name: Alice, Age: 30';
} else if (userId == 'errorUser') {
throw Exception('用户不存在或网络错误'); // 模拟错误情况
} else {
return 'User Data for $userId: No special info.';
}
});
}
// 模拟一个依赖于用户数据的操作
Future<String> processUserData(String data) {
return Future.delayed(Duration(seconds: 1), () {
print('正在处理数据: ${data.substring(0, 15)}...');
return 'Processed Data: ${data.toUpperCase()}';
});
}
void main() {
print('1. 程序开始执行...');
// --- 示例 1: 成功的Future链式调用 ---
print('\n--- 示例 1: 成功获取并处理数据 ---');
fetchUserData('user123')
.then((data) {
print('2. 成功获取用户数据: $data');
return processUserData(data); // 返回一个新的Future,继续链式调用
})
.then((processedData) {
print('3. 成功处理用户数据: $processedData');
})
.catchError((error) {
// 这个catchError不会被触发,因为前面的Future都成功了
print('4. 错误捕获 (不应触发): $error');
})
.whenComplete(() {
print('5. 示例1: 数据获取和处理操作完成 (无论是成功还是失败)');
});
// --- 示例 2: 带有错误的Future链式调用 ---
print('\n--- 示例 2: 带有错误的数据获取 ---');
fetchUserData('errorUser')
.then((data) {
print('6. 成功获取用户数据 (不应触发): $data');
return processUserData(data);
})
.catchError((error) {
print('7. 错误捕获: ${error.toString()}');
// 返回一个新的Future,可以返回一个默认值或者继续抛出错误
return 'Fallback Data: Error occurred.';
})
.then((result) {
print('8. 错误处理后继续执行: $result');
})
.whenComplete(() {
print('9. 示例2: 数据获取和处理操作完成 (无论是成功还是失败)');
});
// --- 示例 3: 没有catchError的错误处理 ---
print('\n--- 示例 3: 没有捕获的错误 ---');
fetchUserData('anotherErrorUser') // 假设这个id也会触发错误
.then((data) {
print('10. 成功获取数据 (不应触发): $data');
})
// 注意这里没有catchError
.whenComplete(() {
print('11. 示例3: 数据获取操作完成 (但错误未被显式捕获)');
});
// 如果没有catchError,且错误没有被后续的then处理,
// 那么未处理的错误最终会被Dart的全局错误处理机制捕获,例如Future.zone
// 在这里,它可能会打印一个"Uncaught Error"到控制台。
print('\n12. main函数结束,异步操作仍在后台进行...');
}
运行结果分析:
1. 程序开始执行...
--- 示例 1: 成功获取并处理数据 ---
--- 示例 2: 带有错误的数据获取 ---
--- 示例 3: 没有捕获的错误 ---
12. main函数结束,异步操作仍在后台进行...
正在处理数据: User Data for user...
2. 成功获取用户数据: User Data for user123: Name: Alice, Age: 30
3. 成功处理用户数据: PROCESSED DATA: USER DATA FOR USER123: NAME: ALICE, AGE: 30
5. 示例1: 数据获取和处理操作完成 (无论是成功还是失败)
7. 错误捕获: Exception: 用户不存在或网络错误
9. 示例2: 数据获取和处理操作完成 (无论是成功还是失败)
8. 错误处理后继续执行: Fallback Data: Error occurred.
11. 示例3: 数据获取操作完成 (但错误未被显式捕获)
Unhandled exception:
Exception: 用户不存在或网络错误
#0 fetchUserData.<anonymous closure> (file:///Users/yourname/Documents/dart_project/lib/main.dart:18:13)
#1 _RootZone.runUnary (dart:async/zone.dart:1679:58)
#2 _FutureListener.handleValue (dart:async/future_impl.dart:140:18)
#3 Future._propagateToListeners.handleValueCallback (dart:async/future_impl.dart:738:45)
#4 Future._propagateToListeners (dart:async/future_impl.dart:767:32)
#5 Future._completeWithValue (dart:async/future_impl.dart:575:5)
#6 _AsyncAwaitCompleter.complete (dart:async/async_manager.dart:28:13)
#7 _AsyncAwaitCompleter.start.<anonymous closure> (dart:async/async_manager.dart:36:15)
#8 _rootHandleDelayed (dart:async/zone.dart:1367:13)
#9 _CustomTimer.call (dart:async/zone.dart:1212:15)
#10 Timer.new. (dart:async/timer.dart:53:23)
#11 _Timer._runTimers (dart:isolate-patch/timer_impl.dart:395:19)
#12 _Timer._handleMessage (dart:isolate-patch/timer_impl.dart:426:7)
#13 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:190:12)
关键观察点:
- 同步代码(
main函数内的print)会首先执行。 fetchUserData调用后立即返回Future,main函数继续执行,不会等待。then回调会在Future成功完成时才执行,按顺序链式执行。catchError会捕获到链条中之前Future抛出的错误,一旦捕获并处理,错误链就会中断,后续的then会继续执行。whenComplete无论成功失败都会在then或catchError之后执行。- 示例3中,因为没有
catchError,错误会上浮,最终导致未捕获异常。
注意事项
-
链式调用与返回
Future:
注意: 当你then方法的回调函数(即onValue函数)又返回一个Future时,then方法会等待这个新的Future完成,然后才会把它的结果传递给下一个then。这是实现异步操作序列的关键。如果回调函数返回一个非Future的值,那么下一个then会立即带着这个值执行。 -
错误传播机制:
注意: 一个Future链中的错误会向下传播,直到遇到一个catchError或者onError回调来处理它。如果没有被处理,它最终会成为一个未捕获的错误。catchError处理错误后,可以返回一个新的值,使后续的.then()链能继续执行。 -
Future不是并发:
注意:Future只是表示一个异步操作的最终结果,它本身并没有创建新的执行线程。Dart仍然运行在单线程上。Future只是将耗时操作的回调安排到事件队列中,在主线程空闲时执行。真正的并发在Dart中是通过Isolates来实现的(我们稍后会讲到)。 -
立即执行与延迟监听:
注意: 一旦创建Future对象(例如Future(() => ...)或Future.delayed(...)),它所包裹的异步操作就会立即开始执行。then、catchError等方法只是注册了在Future完成时要执行的回调,而不是启动Future。 -
FutureOr<T>:
注意: 在Dart中,很多接受Future或普通值的地方,你会看到类型是FutureOr<T>。这意味着该参数或返回值可以是Future<T>,也可以是T类型。这是为了简化API,使得你可以灵活地传递同步或异步的值。例如,then的回调就可以返回一个Future<R>或R。
async和await: 简化异步代码
理论
async和await是Dart语言用于更优雅、更直观地处理Future的关键字。它们使得异步代码看起来和写起来更像同步代码,极大地提高了代码的可读性和可维护性,避免了Future链式调用(.then().then()...)可能导致的“回调地狱(callback hell)”。
-
async关键字:async关键字用于标记一个函数是异步的。async函数的返回类型必须是Future。即使你显式地返回一个非Future的值,Dart也会自动将其包装在Future.value()中。- 在
async函数内部,你可以使用await关键字。
-
await关键字:await关键字只能在async函数内部使用。await后面必须跟随一个Future表达式。- 当
await表达式执行时,它会暂停当前async函数的执行,直到其后面的Future完成(无论是成功还是失败)。 - 一旦
Future完成,await表达式会返回Future的结果值(如果成功),或者抛出Future的错误 (如果失败)。 - 重要:
await仅暂停当前async函数的执行,而不会阻塞整个主线程。事件循环可以继续处理其他任务。这就是为什么async/await被称为“语法糖(syntactic sugar)”来简化Future的使用,而不是改变Dart的单线程异步本质。
async函数的返回行为:
- 如果
async函数正常返回一个值(例如return 'Hello';),那么这个函数返回的Future会以这个值成功完成。 - 如果
async函数抛出一个异常(例如throw Exception('Error!');),那么这个函数返回的Future会以这个异常失败完成。
现实开发场景类比: 继续用外卖的比喻。
-
传统方式(
.then()):
你: 打电话下单。
外卖平台: 好的,请稍等(返回一个Future)。
你: (当外卖平台说好了之后)就吃。
外卖平台: (当吃完了之后)就洗碗。 -
async/await方式:
你:async外卖到家函数。
你:await调用下单函数。
你: 吃。
你: 洗碗。
await让你感觉就像外卖在同一时间送到,然后你继续做下一件事情。虽然本质上还是异步的,但代码逻辑变得线性化了,大大降低了理解成本。
示例
import 'dart:async';
// 模拟一个耗时操作,返回Future<String>
Future<String> downloadFile(String url) async {
print(' 开始下载: $url...');
await Future.delayed(Duration(seconds: 3)); // 模拟网络延迟
print(' 下载完成: $url');
return 'Content_of_file_from_$url';
}
// 模拟另一个耗时操作,返回Future<bool>
Future<bool> processData(String data) async {
print(' 开始处理数据...');
await Future.delayed(Duration(seconds: 2)); // 模拟CPU密集型操作
print(' 数据处理完成。');
return data.length > 10;
}
// 模拟一个可能失败的操作
Future<String> fetchConfig(String configName) async {
print(' 开始获取配置: $configName...');
await Future.delayed(Duration(seconds: 1));
if (configName == 'critical') {
throw Exception('无法获取关键配置: $configName'); // 模拟错误
}
print(' 获取配置成功: $configName');
return 'ConfigValue for $configName';
}
// --- 使用 async/await 的函数 ---
Future<void> performTaskSequence() async {
print('--- 开始 performTaskSequence ---');
try {
String fileContent = await downloadFile('http://example.com/data.txt');
print('接收到文件内容: ${fileContent.substring(0, 15)}...');
String config = await fetchConfig('default');
print('接收到配置: $config');
bool processResult = await processData(fileContent);
print('数据处理结果: $processResult');
// 可以在await之后直接使用结果,如同同步代码
if (processResult) {
print('任务成功完成!');
} else {
print('数据未通过处理验证。');
}
} catch (e) {
print('! ! ! 任务中发生错误: $e');
} finally {
print('--- performTaskSequence 结束 (清理工作) ---');
}
}
// --- 带有预期错误的 async/await 函数 ---
Future<void> performTaskWithError() async {
print('\n--- 开始 performTaskWithError (模拟错误) ---');
try {
String fileContent = await downloadFile('http://example.com/another.txt');
print('接收到文件内容: ${fileContent.substring(0, 15)}...');
// 这里会抛出异常
String criticalConfig = await fetchConfig('critical');
print('接收到关键配置: $criticalConfig'); // 这行代码将不会被执行
bool processResult = await processData(fileContent + criticalConfig);
print('数据处理结果: $processResult');
} catch (e) {
// 错误会被这里的catch捕获
print('! ! ! performTaskWithError 捕获到错误: $e');
} finally {
print('--- performTaskWithError 结束 (清理工作) ---');
}
}
void main() {
print('主函数开始');
// 调用异步函数
performTaskSequence();
performTaskWithError();
print('主函数结束 (异步任务仍在后台运行)');
}
/*
预期输出大致顺序 (具体时间点取决于 Future.delayed):
主函数开始
--- 开始 performTaskSequence ---
开始下载: http://example.com/data.txt...
--- 开始 performTaskWithError (模拟错误) ---
开始下载: http://example.com/another.txt...
主函数结束 (异步任务仍在后台运行)
// 3秒后 (downloadFile完成)
下载完成: http://example.com/data.txt
接收到文件内容: Content_of_fil...
开始获取配置: default...
下载完成: http://example.com/another.txt
接收到文件内容: Content_of_fil...
开始获取配置: critical...
// 1秒后 (fetchConfig完成)
获取配置成功: default
接收到配置: ConfigValue for default
开始处理数据...
开始获取配置: critical... (这里会报错,但async/await函数会捕获)
// 2秒后 (processData完成,同时criticalConfig报错)
数据处理完成。
数据处理结果: true
任务成功完成!
--- performTaskSequence 结束 (清理工作) ---
! ! ! performTaskWithError 捕获到错误: Exception: 无法获取关键配置: critical
--- performTaskWithError 结束 (清理工作) ---
*/
运行结果分析:
main函数是同步执行的。它会打印“主函数开始”,然后立即调用performTaskSequence()和performTaskWithError(),并打印“主函数结束”。这表明performTaskSequence()和performTaskWithError()是非阻塞的。- 当
performTaskSequence函数内部遇到await downloadFile(...)时,performTaskSequence函数会暂停其执行,但整个应用程序的主线程不会被阻塞。Dart的事件循环会继续执行其他任务(例如performTaskWithError内的代码,或者其他事件队列中的任务)。 - 一旦
downloadFile完成,performTaskSequence会从暂停的地方继续执行。 try-catch-finally块在async函数中的行为与同步代码类似,是处理异步错误的主要方式。任何await表达式抛出的错误都会被最近的catch块捕获。
注意事项
-
async函数的返回类型:
注意:async函数的返回类型总是Future<T>,其中T是你实际要返回的值的类型。如果你返回void,则返回Future<void>。如果你没有显式返回任何值,它会隐式地返回Future<void>。 -
await只能在async函数中使用:
注意: 这是编译器的强制要求。如果你不在async函数中使用await,编译器会报错。如果你在main函数中使用await,那么main函数也必须被标记为async (void main() async { ... })。 -
避免忘记
await:
注意: 如果你调用一个返回Future的函数,但忘记在前面加上await,那么这个Future就会“悬空(fire-and-forget)”。它会在后台独立运行,你将无法获取其结果或捕获其错误。除非你明确想要这种行为,否则这通常是一个错误。
Future<void> doSomethingAsync() async {
print('Async task started');
await Future.delayed(Duration(seconds: 1));
print('Async task finished');
}
void main() {
print('Main started');
doSomethingAsync(); // 忘记 await。doSomethingAsync会立即返回一个Future,但main函数不会等待它。
print('Main finished'); // 这会先于 'Async task finished' 打印
}
await带来的顺序性:
注意:await会强制后面的代码等待前一个Future完成。如果你的多个异步操作之间没有依赖关系,并且你希望它们并行执行以节省总时间,那么不应该使用连续的await。你可以先启动多个Future,然后使用Future.wait等待它们全部完成。我们将在后续章节中介绍这个概念。
异步中的错误处理
理论
在任何复杂的应用程序中,错误处理都是不可或缺的一部分。在Dart的异步编程中,由于操作可能在未来某个时间点完成或失败,错误处理机制与同步代码略有不同。我们已经初步接触了Future.catchError和async/await中的try-catch,现在我们将更系统地讨论。
异步错误源:
一个Future可能因以下原因以错误状态完成:
- 显式抛出异常: 在
Future回调中或async函数中使用throw Exception(...)。 - 运行时错误: 例如空引用(NoSuchMethodError)、类型转换失败(CastError) 等。
处理异步错误的策略:
Future.catchError():
- 在
.then()链式调用中,catchError()方法专门用于处理前面Future链中出现的错误。 - 它接收一个函数作为参数,这个函数会在
Future失败时被调用,并接收到抛出的错误。 catchError()可以选择性地接收一个test函数,用于过滤只处理特定类型的错误。- 返回行为:如果
catchError成功处理了错误(即其回调没有再次抛出错误),它会返回一个新的Future,这个新的Future会以正常值(如果回调返回一个值的话)或null(如果回调没有返回值)成功完成,从而中断错误传播。如果catchError的回调自己又抛出错误,或者test函数返回false,那么错误会继续向下传播。
Future.then(..., onError: ...):
then()方法的第二个可选参数onError也可以用来处理错误。- 它只处理紧邻的这个
Future发生的错误。 - 返回行为: 与
catchError类似,如果onError成功处理了错误,它会返回一个成功的Future,中断错误传播。
try-catch语句配合async/await:
- 这是处理
async函数中最推荐和最直观的方式。 - 你可以像处理同步代码一样,将可能抛出错误的
await表达式包裹在try-catch块中。 try-catch可以捕获由await表达式完成的Future所携带的错误。
Future.whenComplete():
- 无论
Future是成功还是失败,whenComplete()的回调都会被调用。 - 它不处理错误,但提供了一个执行清理或最终逻辑的地方,例如关闭文件句柄、停止加载指示器等。
- 它不会改变
Future的完成状态(成功或失败),只会将原Future的结果传递下去。
未捕获的错误:
如果一个Future完成时带着一个错误,但这个错误在整个Future链的末端都没有被catchError或onError处理,那么这个错误就会成为一个“未捕获的错误”。
- 在命令行Dart应用程序中,未捕获的错误通常会导致程序崩溃并打印堆栈跟踪。
- 在Flutter应用程序中,它会被框架的错误处理机制捕获,例如
runZonedGuarded,可以用来提供统一的错误日志和报告。
示例
import 'dart:async';
// 模拟一个会成功的异步操作
Future<String> successfulFuture() {
return Future.delayed(Duration(milliseconds: 500), () => '数据成功获取');
}
// 模拟一个会失败的异步操作
Future<String> failingFuture() {
return Future.delayed(Duration(seconds: 1), () => throw Exception('网络连接失败!'));
}
// 模拟一个会抛出特定错误的异步操作
Future<int> calculateValue(int a, int b) {
return Future.delayed(Duration(milliseconds: 700), () {
if (b == 0) {
throw ArgumentError('除数不能为零');
}
return a ~/ b;
});
}
// --- 示例 1: 使用 .then(onError: ) 和 .catchError() ---
void demonstrateThenAndCatchError() {
print('\n--- 示例 1: .then(onError: ) 和 .catchError() ---');
// 1.1 使用 then 的 onError 参数
failingFuture().then(
(value) => print('示例1.1 成功: $value (不应打印)'),
onError: (error, stackTrace) { // stackTrace 参数是可选的
print('示例1.1 then(onError): 捕获到错误 - ${error.runtimeType}: ${error}');
// 如果 onError 返回一个值或一个非错误 Future,则链条变为成功
return '从错误中恢复的默认值';
},
).then((value) {
print('示例1.1 then(onError)处理后继续: $value');
}).whenComplete(() {
print('示例1.1 whenComplete 执行');
});
// 1.2 使用 catchError (更推荐用于链式错误处理)
successfulFuture().then((value) {
print('示例1.2-1 成功获取: $value');
// 故意在这里抛出错误,看 catchError 是否能捕获
throw FormatException('数据格式不符!');
}).catchError((error) {
print('示例1.2-1 catchError: 捕获到错误 - ${error.runtimeType}: ${error}');
return '格式错误后的默认值'; // 恢复,后续then会执行
}).then((value) {
print('示例1.2-1 catchError处理后继续: $value');
}).whenComplete(() {
print('示例1.2-1 whenComplete 执行');
});
failingFuture().then((value) {
print('示例1.2-2 成功 (不应打印): $value');
}).catchError((error, stackTrace) {
print('示例1.2-2 catchError: 捕获到错误 - ${error.runtimeType}: ${error}');
// 可以选择重新抛出错误,或不返回任何值(相当于返回 null)
// throw error; // 如果需要将错误继续传递下去
}).whenComplete(() {
print('示例1.2-2 whenComplete 执行');
});
}
// --- 示例 2: 使用 async/await 与 try-catch ---
Future<void> demonstrateAsyncAwaitTryCatch() async {
print('\n--- 示例 2: async/await 与 try-catch ---');
// 2.1 成功情况
try {
String result = await successfulFuture();
print('示例2.1 await成功: $result');
} catch (e) {
print('示例2.1 await捕获到错误 (不应触发): $e');
} finally {
print('示例2.1 finally执行');
}
// 2.2 失败情况
try {
String result = await failingFuture(); // 这里会抛出异常
print('示例2.2 await成功 (不应触发): $result');
} catch (e, s) { // 捕获到 Exception
print('示例2.2 await捕获到错误: ${e.runtimeType}: $e');
// print('堆栈信息: $s'); // 可以打印堆栈信息
} finally {
print('示例2.2 finally执行');
}
// 2.3 捕获特定类型的错误 (on 关键字)
try {
int value = await calculateValue(10, 0); // 这里会抛出 ArgumentError
print('示例2.3 await成功 (不应触发): $value');
} on ArgumentError catch (e) {
print('示例2.3 捕获到特定错误 (ArgumentError): $e');
} catch (e) { // 捕获其他类型错误
print('示例2.3 捕获到其他错误: $e');
} finally {
print('示例2.3 finally执行');
}
}
void main() {
// 设置全局错误处理 (仅作演示,实际应用可能使用runZonedGuarded)
// 这是为了演示如果没有被捕获的Future错误会如何表现。
// 在Flutter中,此种错误会被FlutterError.onError或Zone捕获
// Future.microtask(() { // 使用microtask确保在所有main函数代码执行后
// Zone.current.handleUncaughtError = (Object error, StackTrace stack) {
// print('!!! 全局未捕获异步错误: $error');
// // print('Stack: $stack');
// };
// });
print('程序开始执行');
demonstrateThenAndCatchError();
// 注意,这里调用 performTaskWithError 可能会因为未处理的 Future 错误导致主线程报错
// 如果你需要展示未捕获错误,可以注释掉 `demonstrateAsyncAwaitTryCatch` 内部的 `try-catch`
// 并在 Zone.current.handleUncaughtError 里观察
demonstrateAsyncAwaitTryCatch();
print('主函数结束,异步操作正在进行...');
// 示例 3: 未捕获的错误
print('\n--- 示例 3: 未捕获的错误 ---');
failingFuture().then((value) {
print('示例3 成功 (不应打印): $value');
}); // 这里没有 .catchError 或 .then(onError: )
// 这个错误最终会成为一个未捕获的错误,导致程序在异步操作完成后崩溃或被Zone捕获。
// 在 Dart VM 中,通常会打印一个 "Unhandled exception" 并退出。
}
运行结果分析:
main函数是同步执行的,会迅速打印出同步信息。- 各个
demonstrate...函数被调用后,它们内部的异步操作会开始执行。 .then(onError: )和.catchError()会按预期捕获和处理错误,并决定是否继续链式调用。async/await结合try-catch提供了一种更像同步代码的错误处理流程,无论成功还是失败,都遵循try-catch-finally的规则。- 在“示例 3”中,由于
failingFuture()的结果没有被catchError或onError处理,它变成了一个未捕获的异步错误。在Dart VM中,这会导致一个“Unhandled exception”并终止程序(或在Flutter中被runZonedGuarded捕获)。
注意事项
-
async/await与try-catch的优先级:
注意: 在async函数中使用await时,try-catch优先于Future.catchError。如果await表达式抛出错误,try-catch会立即捕获它,而无需Future.catchError。只有当你没有使用await,而是直接操作Future对象时,或者try-catch内部没有捕获到错误时,catchError才会在Future链中发挥作用。 -
错误处理的粒度:
注意: 选择then(onError: )还是catchError()取决于你的需求。then(onError: )处理的是紧邻前一个Future的错误。catchError()可以在链的任何位置捕获之前所有尚未处理的Future抛出的错误,这使它成为处理整个异步链错误的通用方法。async/await + try-catch是处理一系列await调用的首选方式。
-
whenComplete的错误穿透性:
注意:whenComplete回调会在Future完成(无论成功或失败)时执行。它不会捕获错误,也不会改变Future的成功或失败状态。它只是执行一些操作,并将原始的成功值或错误传递给链中的下一个处理器。 -
全局异步错误处理:
注意: 对于应用程序中的所有未捕获的异步错误,Dart 提供了Zone的概念,特别是runZonedGuarded函数。在Flutter中,FlutterError.onError和PlatformDispatcher.instance.onError也是重要的全局错误捕获机制。这些全局钩子用于记录日志、上报错误,防止应用程序崩溃,并向用户提供友好的反馈。推荐在大型应用中设置一个统一的全局错误处理。 -
Stack Traces(堆栈追踪):
注意: 无论是catchError还是try-catch,你都可以捕获到StackTrace对象。这个对象包含了错误发生时的调用堆栈信息,对于调试和问题定位至关重要。
Stream: 处理多个异步事件序列
理论
Stream是Dart中处理一系列异步事件的对象。与Future(表示单个异步事件)不同,Stream可以随着时间推移产生零个、一个或多个事件。它是一个“异步的Iterable”,你可以监听它来接收数据、错误或完成通知。
Stream的典型应用场景:
- 用户事件: 点击、滑动、键盘输入。
- 数据流: 文件读取、网络接收、数据库查询结果。
- 实时更新: 聊天消息、股票报价、传感器数据。
- 动画帧。
Stream的生命周期:
- 未监听(Unlistened):
Stream处于初始状态,没有消费者。 - 监听中(Active): 有监听者(
StreamSubscription) 正在接收事件。 - 暂停(Paused): 监听者暂停接收事件。
- 已取消(Canceled): 监听者取消订阅。
- 已完成(Done):
Stream不再产生事件。
Stream的类型:
- 单订阅流(
Single-subscription Stream):
- 这个
Stream只能被监听一次。一旦一个Stream被监听,并且它的StreamSubscription被取消,该Stream就关闭了,不能再次被监听。 - 尝试监听一个已经被监听过的单订阅流会导致运行时错误。
- 适用于一次性的数据流,如文件下载、单次网络请求的响应流。
- 广播流(
Broadcast Stream):
- 这个
Stream可以被监听多次。即使有多个监听者同时监听,或者一个监听者取消订阅后另一个监听者重新订阅,Stream仍然可以正常工作。 - 适用于需要在应用程序的多个部分共享的事件,如UI事件、全局状态变化。
- 创建一个广播流:
.asBroadcastStream()方法。
核心概念:
-
listen(void onData(T event), {Function onError, void onDone(), bool cancelOnError}):
这是订阅Stream的主要方法。它返回一个StreamSubscription对象。onData: 当Stream产生新数据时调用的回调。onError: 当Stream产生错误时调用的回调。onDone: 当Stream完成(不再有数据)时调用的回调。cancelOnError: 如果设置为true,当Stream产生错误时,订阅会自动取消。
-
StreamSubscription:
代表一个 Stream 的订阅。通过它,你可以控制订阅的生命周期。pause(): 暂停接收事件。resume(): 恢复接收事件。cancel(): 取消订阅,停止接收事件,并释放资源。isPaused: 检查订阅是否被暂停。
-
async*函数(异步生成器):async*函数是一个创建Stream的非常方便的方式。- 它返回一个
Stream。在函数体内部,你可以使用yield关键字来发送数据事件,使用yield*来转发另一个Stream,或者throw来发送错误事件。 - 当
async*函数执行完毕时,它返回的Stream会自动关闭。
示例
import 'dart:async'; // 导入async库
// --- 示例 1: 使用 StreamController 创建一个单订阅流 ---
class DataGenerator {
final _controller = StreamController<int>();
// Getter 返回 Stream,以便外部可以监听
Stream<int> get stream => _controller.stream;
void startGenerating() {
int count = 0;
Timer.periodic(Duration(seconds: 1), (timer) {
if (!_controller.isClosed) {
count++;
_controller.add(count); // 发送数据事件
if (count == 5) {
timer.cancel();
_controller.addError('数据生成器意外停止!'); // 发送错误事件
_controller.close(); // 关闭Stream,发送onDone事件
}
} else {
timer.cancel(); // 如果Stream已经被关闭,也停止计时器
}
});
}
void dispose() {
_controller.close(); // 清理资源
}
}
// --- 示例 2: async* (异步生成器) 函数创建 Stream ---
Stream<String> countdown(int from) async* {
for (int i = from; i >= 0; i--) {
await Future.delayed(Duration(seconds: 1)); // 模拟异步操作
if (i == 2) {
throw Exception('倒计时过程中发生错误'); // 在 Stream 中抛出错误
}
yield '倒计时: $i'; // 发送数据事件
}
// 函数执行完毕,Stream 会自动关闭
}
// --- 示例 3: 广播流 ---
Stream<int> _broadcastStreamController;
Stream<int> get broadcastCounterStream {
if (_broadcastStreamController == null) {
_broadcastStreamController = StreamController<int>().stream.asBroadcastStream();
int count = 0;
Timer.periodic(Duration(seconds: 2), (timer) {
if (!_broadcastStreamController.isClosed) {
count++;
// 只有在 StreamController 是 StreamController<T> 实例时才能使用 add
// 对于 asBroadcastStream() 转换后的 Stream,需要获取原始的 StreamController
// 这里只是为了演示,实际通常是 StreamController 本身就是广播型的。
// 为了简化,我们假设直接用一个广播型的 StreamController。
// 正确的广播流创建方式应该如下:
final bcController = StreamController<int>.broadcast();
Timer.periodic(Duration(seconds: 2), (timer) {
bcController.add(count++);
});
_broadcastStreamController = bcController.stream; // 重新赋值
} else {
timer.cancel();
}
});
}
return _broadcastStreamController;
}
void main() async {
print('程序开始执行');
// --- 示例 1: 单订阅流的监听 ---
print('\n--- 示例 1: DataGenerator 单订阅流 ---');
final generator = DataGenerator();
// 监听Stream
StreamSubscription<int> subscription = generator.stream.listen(
(data) => print('监听器A收到数据: $data'),
onError: (error) => print('监听器A收到错误: $error'),
onDone: () => print('监听器A完成'),
cancelOnError: true, // 遇到错误时自动取消订阅
);
await Future.delayed(Duration(seconds: 3)); // 让数据生成一段时间
subscription.pause(); // 暂停接收
print('监听器A已暂停');
await Future.delayed(Duration(seconds: 2));
subscription.resume(); // 恢复接收
print('监听器A已恢复');
// 尝试再次监听同一个单订阅流 (会报错)
// try {
// generator.stream.listen((data) => print('监听器B收到数据: $data'));
// } catch(e) {
// print('尝试再次监听单订阅流失败: $e');
// }
await Future.delayed(Duration(seconds: 4)); // 等待 Stream 完成或错误
// --- 示例 2: async* (异步生成器) 流 ---
print('\n--- 示例 2: async* (异步生成器) 流 ---');
await for (var event in countdown(4)) { // 使用 await for 循环消费 Stream
print(event);
}
// await for 循环会自动处理 Stream 的完成和错误
// 它会在 Stream 中遇到错误时抛出异常,因此需要 try-catch 包裹
try {
await for (var event in countdown(4)) { // 再次调用会创建一个新的 Stream
print(event);
}
} catch(e) {
print('async* 流中捕获到错误: $e');
}
// --- 示例 3: 广播流 ---
print('\n--- 示例 3: 广播流 ---');
// 创建一个广播StreamController
final bcController = StreamController<String>.broadcast();
int broadcastCount = 0;
Timer.periodic(Duration(seconds: 1), (timer) {
if (!bcController.isClosed) {
broadcastCount++;
bcController.add('广播事件 $broadcastCount');
if (broadcastCount == 3) {
bcController.addError('广播流错误!');
}
if (broadcastCount == 5) {
timer.cancel();
bcController.close();
}
} else {
timer.cancel();
}
});
// 多个监听者可以同时监听广播流
bcController.stream.listen(
(data) => print('广播监听器X收到: $data'),
onError: (error) => print('广播监听器X收到错误: $error'),
onDone: () => print('广播监听器X完成'),
);
bcController.stream.listen(
(data) => print('广播监听器Y收到: --> $data <--'),
onError: (error) => print('广播监听器Y收到错误: --> $error <--'),
onDone: () => print('广播监听器Y完成'),
);
await Future.delayed(Duration(seconds: 7)); // 确保所有异步操作有时间执行
print('\n程序结束');
generator.dispose(); // 清理资源
bcController.close(); // 清理资源
}
运行结果分析:
main函数是同步执行的,然后启动各种Stream的事件。- “示例 1”展示了一个单订阅流。第一个监听器成功接收数据,暂停,恢复,直到遇到错误后自动取消订阅并进入
onDone。- 注意: 如果你取消注释
try { generator.stream.listen((data) => print('监听器B收到数据: $data')); }部分,你会看到运行时错误,因为generator.stream是一个单订阅流,一旦被监听过,就不能再次被监听。
- 注意: 如果你取消注释
- “示例 2”展示了
async*函数的用法。await for循环非常简洁,它会等待Stream发送的每个事件,直到Stream完成。- 当
async*函数内部抛出错误时,await for循环会捕获这个错误并重新抛出,所以需要try-catch处理。
- “示例 3”展示了广播流的特性: 多个监听器可以同时订阅同一个
Stream,并且都能接收到相同的事件。错误和完成事件也会被所有监听器接收。
注意事项
-
资源管理与
StreamSubscription:
注意: 当你订阅一个Stream(通过listen)时,会得到一个StreamSubscription对象。如果不再需要接收事件,务必调用subscription.cancel()来释放资源,防止内存泄漏。尤其在Flutter的StatefulWidget中,通常在dispose()方法中取消订阅。 -
单订阅流重复监听: 注意: 单订阅流一旦被
listen,就不能再次listen。试图这样做会抛出StateError。如果确实需要多次监听,应该使用stream.asBroadcastStream()转换为广播流。 -
async* vs StreamController:
注意:async*更适合于生成Stream事件序列是基于循环、算法等逻辑的场景。而StreamController更适合外部事件源(如UI事件、网络回调)驱动的场景,你可以手动调用add()、addError()、close()来控制Stream。 -
错误处理:
注意:Stream中的错误通常通过listen方法的onError参数或await for循环的try-catch块来处理。如果cancelOnError设置为true,Stream在遇到错误后会自动关闭。未处理的Stream错误最终也可能成为未捕获的异步错误。 -
StreamController.broadcast():
注意:直接使用StreamController<T>.broadcast()可以创建一个广播型的StreamController,这样它生成的stream已经是广播流,可以被多次监听,而无需使用.asBroadcastStream()转换。 -
Stream操作符(Operators):
注意:Stream类提供了许多有用的方法(称为操作符),如map()、where()、take()、skip()、debounce()、distinct()等,它们允许你转换、过滤、组合和控制Stream的事件流。这使得处理复杂事件序列变得非常强大和灵活。这些操作符通常返回一个新的Stream。
异步模式与高级工具
理论
在掌握了Future和Stream的基础知识后,我们将进一步探索Dart中更高级的异步编程模式和一些有用的工具。这些模式和工具能够帮助我们更高效、更灵活地管理复杂的异步任务,优化性能,并提供更好的用户体验。
核心概念:
Future组合(Future Composition):
当需要协调多个Future的结果时,Dart提供了强大的组合能力。
Future.wait(Iterable<Future<T>> futures):- 等待所有给定的
Future都完成。 - 如果所有
Future都成功完成,Future.wait返回一个包含所有Future结果的List。 - 如果有任何一个
Future失败,Future.wait会立即失败,并返回第一个失败的Future的错误。 - 注意:即使
Future.wait因某个Future失败而提前失败,其余的Future仍然会在后台继续执行,只是它们的成功或失败对Future.wait的结果不再重要。
- 等待所有给定的
Future.any(Iterable<Future<T>> futures):- 等待给定的
Future中任意一个完成(无论是成功还是失败)。 Future.any返回第一个完成的Future的结果(值或错误)。- 注意: 同样,其余的
Future仍然会在后台继续执行。
- 等待给定的
Completer(用于手动控制Future的完成):
Completer<T>是一个用于创建Future并可以手动控制其完成(成功或失败)的对象。- 当你无法通过已有的异步
API获取Future,但你又想暴露一个Future接口时,Completer就很有用。 - 通过
Completer.future获取Future对象。 - 通过
Completer.complete(value)使Future成功完成。 - 通过
Completer.completeError(error, [stackTrace])使Future失败完成。 - 注意: 每个
Completer只能完成一次,尝试多次完成会导致StateError。
Zone(管理和拦截异步操作):
Zone是Dart中一个强大的机制,用于在特定作用域内封装和拦截异步操作。- 你可以为异步代码设置一个
Zone,然后在这个Zone内部捕获并处理所有未捕获的错误,或者拦截print语句、定时器等。 - 最常见的用途是使用
runZonedGuarded来全局捕获异步错误,防止应用程序崩溃,并进行统一的错误报告。
Isolates(处理计算密集型任务):
- Dart语言是单线程模型。这意味着即使有多个
Future和Stream在并行执行,它们仍然运行在同一个事件循环(Event Loop)和同一个内存空间中。 - 对于计算密集型任务(如复杂的数据处理、图像处理),如果直接在主线程执行,会导致UI阻塞,应用程序无响应。
Isolate允许Dart代码在独立的线程中运行,每个Isolate都有自己的内存空间和事件循环。它们之间通过消息传递进行通信。Isolate之间不共享内存,因此无需担心共享数据锁和竞态条件,但消息传递会有一定的开销。- 注意:
Isolate在Flutter中非常重要,例如处理耗时操作,但不直接用于UI交互。
示例
import 'dart:async';
import 'dart:isolate'; // 导入Isolate库
import 'dart:math';
// --- 辅助函数:模拟耗时操作 ---
Future<String> _fetchData(String name, int duration, {bool fail = false}) async {
print(' 开始获取数据 ($name), 预计等待 ${duration}ms...');
await Future.delayed(Duration(milliseconds: duration));
if (fail && Random().nextBool()) { // 随机失败
throw Exception('从 $name 获取数据失败!');
}
print(' 结束获取数据 ($name)');
return '$name_Data';
}
// --- 示例 1: Future 组合 ---
Future<void> demonstrateFutureComposition() async {
print('\n--- 示例 1: Future 组合 ---');
// Future.wait 示例
print(' -- demonstrateFuture.wait --');
try {
List<String> results = await Future.wait([
_fetchData('SourceA', 2000),
_fetchData('SourceB', 1000),
_fetchData('SourceC', 1500),
]);
print(' 所有数据获取成功: $results'); // ['SourceA_Data', 'SourceB_Data', 'SourceC_Data']
} catch (e) {
print(' Future.wait 捕获到错误: $e'); // 如果有任何一个 Future 失败
}
// Future.any 示例
print(' -- demonstrateFuture.any --');
try {
String firstResult = await Future.any([
_fetchData('FastAPI', 800),
_fetchData('SlowAPI', 3000),
_fetchData('FailureAPI', 500, fail: true), // 可能更快失败
]);
print(' 第一个完成的数据是: $firstResult');
} catch (e) {
print(' Future.any 捕获到错误 (第一个完成的是错误): $e');
}
}
// --- 示例 2: Completer 的使用 ---
class Authenticator {
final _loginCompleter = Completer<String>();
// 暴露一个Future给外部等待
Future<String> get onLoginCompleted => _loginCompleter.future;
// 模拟登录过程,在某个时间点完成Future
void login(String username, String password) async {
print(' 尝试登录: $username...');
await Future.delayed(Duration(seconds: 2));
if (username == 'admin' && password == 'password') {
_loginCompleter.complete('登录成功!欢迎 $username'); // 成功完成
} else {
_loginCompleter.completeError(Exception('用户名或密码错误!')); // 失败完成
}
}
}
Future<void> demonstrateCompleter() async {
print('\n--- 示例 2: Completer 的使用 ---');
final authenticator = Authenticator();
// 在异步函数中 await 等待登录结果
authenticator.onLoginCompleted.then((message) {
print(' 监听登录成功: $message');
}).catchError((error) {
print(' 监听登录失败: $error');
});
// 模拟在另一个地方触发登录
await Future.delayed(Duration(milliseconds: 500));
authenticator.login('guest', '123'); // 这次会失败
await Future.delayed(Duration(seconds: 3)); // 等待上一个完成
// 注意:Completer无法再次complete,会抛出StateError
// authenticator._loginCompleter.complete('第二次尝试');
}
// --- 示例 3: Zone 全局错误处理 ---
void someBuggyAsyncCode() async {
await Future.delayed(Duration(milliseconds: 100));
throw Exception("这是 Zones 内部的一个未捕获的异步错误!");
}
void demonstrateZones() {
print('\n--- 示例 3: Zone 全局错误处理 ---');
// runZonedGuarded 捕获在这个 Zone 内所有未被 try-catch 捕获的异步错误
runZonedGuarded(() async {
print(' 在 runZonedGuarded 内部执行异步代码...');
await Future.delayed(Duration(milliseconds: 50));
someBuggyAsyncCode(); // 这里会抛出未捕获的异步错误
print(' 如果错误被捕获,这句可能不会立即打印。');
}, (error, stack) {
print(' Zone 捕获到未处理的错误: $error');
// 在生产环境中,你会在这里上报错误日志
// print(' 堆栈追踪: $stack');
});
print(' main 线程继续执行,Zone 内部错误已被捕获。');
}
// --- 示例 4: Isolates (计算密集型任务) ---
// Isolate 入口函数(必须是顶层函数或静态方法)
void expensiveComputation(SendPort sendPort) {
print(' Isolate 1: 开始计算...');
int sum = 0;
for (int i = 0; i < 1000000000; i++) { // 模拟大量计算
sum += i;
}
print(' Isolate 1: 计算完成,结果 $sum');
sendPort.send(sum); // 将结果发送回主 Isolate
}
void demonstrateIsolates() async {
print('\n--- 示例 4: Isolates ---');
print(' 主 Isolate: 启动昂贵计算...');
// 1. 创建接收端口
ReceivePort receivePort = ReceivePort();
// 2. 启动新的 Isolate,并传入发送端口
// Isolate.spawn 返回一个 Future<Isolate>
// 该 Future 在新 Isolate 启动后完成
Isolate isolate = await Isolate.spawn(
expensiveComputation,
receivePort.sendPort,
);
// 3. 监听接收端口,等待 Isolate 返回结果
receivePort.listen((message) {
print(' 主 Isolate: 收到 Isolate 发送的结果: $message');
isolate.kill(priority: Isolate.immediate); // 接收到消息后,杀死 Isolate
receivePort.close();
});
print(' 主 Isolate: 可以继续执行其他任务,不会阻塞 UI。');
// 模拟主 Isolate 上的其他操作
await Future.delayed(Duration(milliseconds: 100));
print(' 主 Isolate: 继续做一些轻量级工作...');
}
void main() async {
print('程序开始');
await demonstrateFutureComposition();
await demonstrateCompleter();
demonstrateZones(); // Zones 内部有 await,所以这里不需要 await
await demonstrateIsolates(); // Isolates 内部有 await,所以这里需要 await
print('\n所有异步演示结束。');
}
运行结果分析:
Future.wait: 会等待SourceA,SourceB,SourceC都完成,然后打印出结果列表。如果其中任何一个_fetchData失败,整个Future.wait会立即进入catch块。Future.any: 会等待FastAPI,SlowAPI,FailureAPI中最快完成的一个。如果FailureAPI失败得最快,那么Future.any就会返回这个错误。Completer:Authenticator类通过Completer提供了onLoginCompleted这个Future。外部代码可以await这个Future或者.then().catchError()它。login方法根据业务逻辑调用_loginCompleter.complete()或_loginCompleter.completeError()来完成这个Future。Zone:runZonedGuarded示例演示了如何捕获异步代码中未被try-catch处理的错误。即使someBuggyAsyncCode在不同的事件循环时机抛出异常,runZonedGuarded也能捕获并处理它,而不会导致整个程序崩溃。Isolates:demonstrateIsolates启动了一个新的Isolate来执行CPU密集型任务。主Isolate不会被阻塞,可以继续打印“继续做一些轻量级工作...”,同时副Isolate在其自身的线程中默默计算。当副Isolate完成计算并将结果发送回来时,主Isolate通过receivePort.listen收到结果。
注意事项
-
Future.wait的错误处理:
注意:Future.wait只有在所有Future都成功时才返回成功。只要有一个Future失败,它就立即返回第一个失败的错误。这意味着如果你希望所有Future都执行完毕,无论成功与否,你需要在每个子Future内部自行处理错误(例如.catchError(() => defaultValue)),或者让Future.wait失败,然后检查原始Future列表的状态。 -
Completer的单次完成限制:
注意:Completer只能完成一次(complete或completeError)。第二次尝试完成会抛出StateError。在使用Completer时要确保其完成逻辑的正确性。 -
Zone的性能开销:
注意:Zone机制虽然强大,但会有一定的性能开销。因此,不应滥用Zone,而应在需要全局错误处理、日志记录或特定副作用管理时慎重使用。runZonedGuarded是一个最佳实践。 -
Isolates的通信成本:
注意:Isolates之间通过消息传递进行通信,这涉及到数据的序列化与反序列化,会有一定的开销。因此,只有当计算任务确实是CPU密集型,并且其计算时间远超消息传递开销时,使用Isolate才能带来性能优势。对于少量或频繁的通信,Future和async/await仍然是首选。Isolates通常不用于更新UI,因为UI只能在主Isolate上更新。 -
main函数的异步性:
注意: 如果你的main函数内部有await调用,main函数也应该被标记为async (void main() async { ... })。 -
Stream组合与转换:
注意:Stream也提供了丰富的组合和转换方法(where,map,asyncMap,switchMap,buffer,throttle,debounce等),能够处理复杂的事件流。这些高级Stream操作通常通过package:rxdart等响应式编程库得到增强。