Kotlin协程并发实战用async/await并行处理多个API请求当移动应用首页需要同时加载用户信息、广告Banner和推荐内容时传统的串行请求方式会让用户等待所有接口依次返回。这种体验在网速较慢时尤为明显。Kotlin协程的async/await组合为我们提供了一种优雅的并发解决方案能够将多个独立请求并行发送显著缩短总等待时间。1. 理解并发与挂起的本质区别很多开发者容易混淆挂起suspend和并发concurrency这两个概念。挂起是指协程在执行到某个耗时操作时能够自动让出线程资源而不阻塞线程而并发则是指多个任务真正同时执行的能力。关键区别挂起函数按顺序执行时总耗时是各函数耗时的累加并发执行的挂起函数总耗时约等于最慢的那个函数耗时考虑以下两种实现方式// 顺序执行版本 suspend fun fetchSequentially() { val user getUserInfo() // 耗时200ms val banners getBanners() // 耗时300ms val recommends getRecommends() // 耗时400ms // 总耗时约900ms } // 并发执行版本 suspend fun fetchConcurrently() coroutineScope { val userDeferred async { getUserInfo() } val bannersDeferred async { getBanners() } val recommendsDeferred async { getRecommends() } val user userDeferred.await() val banners bannersDeferred.await() val recommends recommendsDeferred.await() // 总耗时约400ms }2. async/await的核心工作机制async启动一个新的子协程并立即返回一个Deferred对象这个对象代表一个未来的结果。当我们调用await()时如果结果已经就绪直接返回如果结果尚未就绪挂起当前协程而不阻塞线程当结果可用时恢复协程执行典型误区纠正async不是立即开始执行 - 它取决于协程调度器多个async块默认是并发执行的除非特意限制await()调用顺序不影响实际执行顺序3. 实战首页多接口并行加载让我们通过一个完整的ViewModel实现来展示最佳实践class HomeViewModel : ViewModel() { private val repo HomeRepository() val uiState MutableStateFlowHomeUiState(HomeUiState.Loading) fun loadHomeData() { viewModelScope.launch { try { val (user, banners, recommends) fetchAllConcurrently() uiState.value HomeUiState.Success(user, banners, recommends) } catch (e: Exception) { uiState.value HomeUiState.Error(e.message) } } } private suspend fun fetchAllConcurrently() coroutineScope { val userDeferred async { repo.getUserInfo() } val bannersDeferred async { repo.getBanners() } val recommendsDeferred async { repo.getRecommends() } Triple(userDeferred.await(), bannersDeferred.await(), recommendsDeferred.await()) } }性能对比数据请求方式用户信息(ms)Banner(ms)推荐内容(ms)总耗时(ms)串行执行200300400900并行执行2003004004004. 异常处理与取消机制并发场景下的异常处理需要特别注意private suspend fun fetchAllSafely() coroutineScope { val userDeferred async { try { repo.getUserInfo() } catch (e: Exception) { null // 返回默认值而不传播异常 } } val bannersDeferred async { repo.getBanners().onFailure { emptyList() // 返回空列表而不中断流程 }.getOrThrow() } // 如果任一await抛出未捕获异常其他未完成的async会被自动取消 val recommends try { async { repo.getRecommends() }.await() } catch (e: Exception) { emptyList() } HomeData(userDeferred.await(), bannersDeferred.await(), recommends) }取消传播规则当父协程被取消时所有子协程都会被自动取消某个async协程抛出未捕获异常时同级其他协程也会被取消使用SupervisorJob可以避免异常传播5. 高级优化技巧5.1 限制并发数量当需要处理大量任务时可以使用Semaphore控制最大并发数private suspend fun fetchMultipleItems(items: ListItemId) coroutineScope { val semaphore Semaphore(5) // 最大5个并发请求 items.map { itemId - async { semaphore.withPermit { // 获取许可 repo.getItemDetails(itemId) } } }.awaitAll() // 等待所有结果 }5.2 结果缓存策略对于不常变化的数据可以结合CacheAsync模式private val userCache mutableMapOfUserId, DeferredUser() suspend fun getUserWithCache(userId: UserId) coroutineScope { val deferred userCache.getOrPut(userId) { async { repo.getUser(userId) }.also { // 10分钟后失效 launch { delay(10.minutes) userCache.remove(userId) } } } deferred.await() }5.3 差异化超时设置为不同重要性的请求设置不同超时suspend fun fetchWithTimeouts() coroutineScope { val userDeferred async { withTimeout(500.ms) { getUserInfo() } } val bannersDeferred async { withTimeout(1.seconds) { getBanners() } } // 次要内容允许更长时间 val recommendsDeferred async { withTimeout(2.seconds) { getRecommends() } } // ... }在实际项目中我发现合理设置超时比单纯追求并发数量更重要。曾经遇到过因为某个非关键接口响应慢导致整个页面加载卡顿的情况后来通过为每个请求设置适当的独立超时显著提升了用户体验。
Kotlin协程并发实战:5分钟学会用async/await并行处理多个API请求
Kotlin协程并发实战用async/await并行处理多个API请求当移动应用首页需要同时加载用户信息、广告Banner和推荐内容时传统的串行请求方式会让用户等待所有接口依次返回。这种体验在网速较慢时尤为明显。Kotlin协程的async/await组合为我们提供了一种优雅的并发解决方案能够将多个独立请求并行发送显著缩短总等待时间。1. 理解并发与挂起的本质区别很多开发者容易混淆挂起suspend和并发concurrency这两个概念。挂起是指协程在执行到某个耗时操作时能够自动让出线程资源而不阻塞线程而并发则是指多个任务真正同时执行的能力。关键区别挂起函数按顺序执行时总耗时是各函数耗时的累加并发执行的挂起函数总耗时约等于最慢的那个函数耗时考虑以下两种实现方式// 顺序执行版本 suspend fun fetchSequentially() { val user getUserInfo() // 耗时200ms val banners getBanners() // 耗时300ms val recommends getRecommends() // 耗时400ms // 总耗时约900ms } // 并发执行版本 suspend fun fetchConcurrently() coroutineScope { val userDeferred async { getUserInfo() } val bannersDeferred async { getBanners() } val recommendsDeferred async { getRecommends() } val user userDeferred.await() val banners bannersDeferred.await() val recommends recommendsDeferred.await() // 总耗时约400ms }2. async/await的核心工作机制async启动一个新的子协程并立即返回一个Deferred对象这个对象代表一个未来的结果。当我们调用await()时如果结果已经就绪直接返回如果结果尚未就绪挂起当前协程而不阻塞线程当结果可用时恢复协程执行典型误区纠正async不是立即开始执行 - 它取决于协程调度器多个async块默认是并发执行的除非特意限制await()调用顺序不影响实际执行顺序3. 实战首页多接口并行加载让我们通过一个完整的ViewModel实现来展示最佳实践class HomeViewModel : ViewModel() { private val repo HomeRepository() val uiState MutableStateFlowHomeUiState(HomeUiState.Loading) fun loadHomeData() { viewModelScope.launch { try { val (user, banners, recommends) fetchAllConcurrently() uiState.value HomeUiState.Success(user, banners, recommends) } catch (e: Exception) { uiState.value HomeUiState.Error(e.message) } } } private suspend fun fetchAllConcurrently() coroutineScope { val userDeferred async { repo.getUserInfo() } val bannersDeferred async { repo.getBanners() } val recommendsDeferred async { repo.getRecommends() } Triple(userDeferred.await(), bannersDeferred.await(), recommendsDeferred.await()) } }性能对比数据请求方式用户信息(ms)Banner(ms)推荐内容(ms)总耗时(ms)串行执行200300400900并行执行2003004004004. 异常处理与取消机制并发场景下的异常处理需要特别注意private suspend fun fetchAllSafely() coroutineScope { val userDeferred async { try { repo.getUserInfo() } catch (e: Exception) { null // 返回默认值而不传播异常 } } val bannersDeferred async { repo.getBanners().onFailure { emptyList() // 返回空列表而不中断流程 }.getOrThrow() } // 如果任一await抛出未捕获异常其他未完成的async会被自动取消 val recommends try { async { repo.getRecommends() }.await() } catch (e: Exception) { emptyList() } HomeData(userDeferred.await(), bannersDeferred.await(), recommends) }取消传播规则当父协程被取消时所有子协程都会被自动取消某个async协程抛出未捕获异常时同级其他协程也会被取消使用SupervisorJob可以避免异常传播5. 高级优化技巧5.1 限制并发数量当需要处理大量任务时可以使用Semaphore控制最大并发数private suspend fun fetchMultipleItems(items: ListItemId) coroutineScope { val semaphore Semaphore(5) // 最大5个并发请求 items.map { itemId - async { semaphore.withPermit { // 获取许可 repo.getItemDetails(itemId) } } }.awaitAll() // 等待所有结果 }5.2 结果缓存策略对于不常变化的数据可以结合CacheAsync模式private val userCache mutableMapOfUserId, DeferredUser() suspend fun getUserWithCache(userId: UserId) coroutineScope { val deferred userCache.getOrPut(userId) { async { repo.getUser(userId) }.also { // 10分钟后失效 launch { delay(10.minutes) userCache.remove(userId) } } } deferred.await() }5.3 差异化超时设置为不同重要性的请求设置不同超时suspend fun fetchWithTimeouts() coroutineScope { val userDeferred async { withTimeout(500.ms) { getUserInfo() } } val bannersDeferred async { withTimeout(1.seconds) { getBanners() } } // 次要内容允许更长时间 val recommendsDeferred async { withTimeout(2.seconds) { getRecommends() } } // ... }在实际项目中我发现合理设置超时比单纯追求并发数量更重要。曾经遇到过因为某个非关键接口响应慢导致整个页面加载卡顿的情况后来通过为每个请求设置适当的独立超时显著提升了用户体验。