看似相同的代码行为却截然不同
以下两段代码看起来几乎一样:
withContext(Dispatchers.IO) {
// work
}launch(Dispatchers.IO) {
// work
}
它们运行在同一个调度器上。它们都将工作转移到后台线程。但它们的行为并不相同。
以下是并排运行它们的结果:
fun main() = runBlocking {
println("Start")
launch(Dispatchers.IO) {
delay(100)
println("Inside launch")
}
withContext(Dispatchers.IO) {
delay(50)
println("Inside withContext")
}
println("End")
}
输出:
Start
Inside withContext
End
Inside launch
这展示了关键区别。
withContext 会等待其工作完成才会继续执行。
launch 则完全不等待——它并行运行,并在稍后完成。
为什么会发生这种情况
要理解这一差异,我们需要深入协程的内部机制。这两个构造函数的核心区别在于它们如何处理“挂起”与“启动”的逻辑。
withContext 是一个挂起函数。这意味着调用它的协程会暂停执行,直到指定的调度器上的代码块执行完毕。它本质上是一个同步阻塞点,尽管是在异步上下文中。你可以将其视为一个“等待并获取结果”的操作。在上面的示例中,主协程在调用 withContext 后,必须等待后台任务完成,然后才能打印“Done with context”。这种顺序执行保证了数据的一致性,适用于需要确保操作完成后再进行下一步的场景。
相比之下,launch 是一个返回 Job 实例的函数。它启动一个新的协程,但不会暂停当前协程的执行。调用 launch 后,代码会立即继续执行下一行。在示例中,主协程在启动后台任务后,立即打印“Done with launch”,而此时后台任务可能还在运行。这就是所谓的“fire-and-forget”(即发即忘)模式。虽然它启动了并行工作,但调用者并不关心任务何时完成,除非手动获取 Job 并调用 join()。
这种差异在 Android 开发中至关重要。如果你使用 withContext 来更新 UI 状态,你可以确保在函数返回时,状态已经更新完毕。而如果你使用 launch 来执行网络请求,你需要通过回调、LiveData 或 StateFlow 来监听结果,因为 launch 本身不会等待结果返回。混淆这两者可能导致竞态条件:例如,在 launch 启动的网络请求完成之前,UI 尝试读取数据,从而获取到旧值或空值。
因此,选择哪个函数取决于你是否需要等待结果。如果需要同步等待,使用 withContext;如果需要异步并行执行且不立即关心结果,使用 launch。理解这一点,能帮助你写出更健壮、更可预测的协程代码。
这种差异在实际项目中的重要性
在许多 Android 场景中,任务完成的确切时间至关重要。哪怕是顺序上的细微差别,都可能导致一些难以察觉的 bug,直到生产环境才会被发现。
1. 后台任务完成后更新 UI
在 ViewModel 中:
viewModelScope.launch {
val user = withContext(Dispatchers.IO) {
userRepository.loadUser()
}
_state.value = user
}
输出(概念图):
User loaded first
UI updated second
由于 withContext 会等待,因此 UI 只有在加载完成后才会更新。这种同步机制确保了用户界面状态的最终一致性,避免了在数据尚未就绪时渲染错误信息或空白状态。开发者可以确信,当代码执行到更新 UI 的逻辑时,后台耗时操作已经彻底结束,数据已完全就绪。
但如果将其替换为 launch(Dispatchers.IO):
viewModelScope.launch {
var result: User = null
launch(Dispatchers.IO) {
result = userRepository.loadUser()
}
_state.value = result
}
输出(概念图):
UI updated first
User loaded later
这里,UI 在后台任务完成之前就更新了。语法上的视觉相似性掩盖了执行顺序的不同。这意味着主线程不会阻塞,而是立即继续执行后续逻辑。如果后续逻辑依赖于后台任务的结果,就会读取到旧数据或空值,从而引发界面显示异常。这种“触发并继续”的模式虽然提高了响应速度,但也引入了竞态条件的风险,要求开发者手动处理状态同步。
2.协调多步骤后台操作
在自定义仓库代码中,你可能需要操作严格按照顺序执行。例如,先保存数据,然后写入日志:
withContext(Dispatchers.IO) {
fileWriter.save(data)
}
withContext(Dispatchers.IO) {
logWriter.write("Saved")
}
3. 访问共享状态
使用 withContext 进行顺序执行在更新共享对象时更安全:
fun saveAndLog() {
withContext(Dispatchers.IO) {
saveData()
logAction()
}
}
使用 launch,两个更新操作可以同时运行:
fun saveAndLog() {
launch(Dispatchers.IO) { saveData() }
launch(Dispatchers.IO) { logAction() }
}
除非缓存本身是线程安全的,否则并行写入可能会导致竞态条件。
为什么这种差异容易被忽略
主要原因是语法。两者都使用相同的括号。两者都显示 Dispatchers.IO。两者都将代码包裹在代码块中。人们的注意力会集中在调度器上,而不是协程构建器上。
另一个原因是许多现代库已经在内部处理线程。Room DAO 方法和 Retrofit 的挂起函数不需要 手动切换调度器。由于这些库减少了手动切换调度器,开发者看到的协程构建器之间的明显差异较少。
这可能会造成一种错觉,即所有调度器的用法都可以互换,但对于自定义操作来说并非如此。
与 async/await 的深入比较
为了进一步突出差异,请比较以下三个示例:
// 示例 A
val deferred = async { fetchData() }
val result = deferred.await()
// 示例 B
val job = launch { fetchData() }
job.join()
// 示例 C
withContext(Dispatchers.IO) { fetchData() }
输出(概念图):
A: 异步启动 -> 等待结果
B: 启动 -> 阻塞等待完成
C: 同步阻塞当前协程
async 的行为类似于 launch,但返回 Deferred。
await 会将其转换为顺序行为——就像 join 一样。
关键点:顺序行为仅在显式等待时发生。
异常处理行为
withContext 直接在调用协程中抛出异常。它们不会被延迟或存储。
launch 将异常报告给其父作用域。如果该作用域有 supervisor 或自定义异常处理程序,则效果不同。异常不会立即中断调用者的执行路径。
这意味着:
withContext(Dispatchers.IO) { error("Boom") }
println("Next")
崩溃会在“Next”执行之前停止协程。
但使用 launch:
launch(Dispatchers.IO) { error("Boom") }
println("Next")
“Next”仍然会打印。启动的协程会自行崩溃。
需要记住的内容
-
withContext等待代码块执行完毕后才会继续执行。 -
launch会启动并行工作并立即返回。 -
当结果或顺序至关重要时,请使用
withContext。 -
当你需要并发工作且无需阻塞调用者时,请使用
launch。 -
视觉上的相似性掩盖了行为上的差异——协程构建器定义了语义,而不是调度器。
总结
withContext(Dispatchers.IO) 和 launch(Dispatchers.IO) 看起来相似,但行为却不同。前者会暂停并确保顺序执行,而后者会创建并发并立即继续执行。当工作顺序、共享状态、UI 更新或异常处理依赖于工作完成时间时,这种差异至关重要。
理解这种区别可以使协程代码更易于预测,并避免那些只有在实际应用中才会显现的细微错误。
欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!
保护原创,请勿转载!
launch(Dispatchers.IO) { fileWriter.save(data) }
launch(Dispatchers.IO) { logWriter.write("Saved") }
withContext(Dispatchers.IO) {
cache.update(item)
}
launch(Dispatchers.IO) { cache.update(item1) }
launch(Dispatchers.IO) { cache.update(item2) }
val deferred = async(Dispatchers.IO) {
loadData()
}val result = deferred.await()
println(result)
loadData completes
result printed
Dispatchers.IO
withContext(Dispatchers.IO)
launch(Dispatchers.IO)
withContext
Dispatchers.IO
launch
join()
withContext
launch
suspend
launch
withContext
launch
Dispatchers.IO
withContext(Dispatchers.IO)
async
launch
Deferred
await()
withContext









