协程基础
Kotlin协程入门
概述
Kotlin 协程是轻量级的并发方案,通过挂起函数实现非阻塞异步编程。协程的创建和调度成本远低于线程,可以在单线程上运行数万个协程。本文介绍协程的基础概念、启动方式和作用域管理。
基础概念
协程核心术语
| 术语 | 说明 |
|---|---|
| CoroutineScope | 协程作用域,控制协程的生命周期 |
| launch | 启动协程,返回 Job(不返回结果) |
| async | 启动协程,返回 Deferred(可获取结果) |
| suspend | 挂起函数标记,表示可暂停执行 |
| Dispatcher | 调度器,决定协程在哪个线程执行 |
| Job | 协程的句柄,可取消和等待 |
调度器类型
| 调度器 | 用途 |
|---|---|
| Dispatchers.Default | CPU 密集型任务 |
| Dispatchers.IO | I/O 操作 |
| Dispatchers.Main | UI 线程(Android) |
| Dispatchers.Unconfined | 不限定线程 |
快速上手
启动协程
import kotlinx.coroutines.*
// launch — 启动协程(不返回结果)
runBlocking {
val job = launch {
delay(1000)
println("协程执行完成")
}
job.join() // 等待协程完成
}
// async — 启动协程(返回结果)
runBlocking {
val deferred = async { fetchUser() }
val user = deferred.await() // 获取结果
println(user)
}
协程作用域
// viewModelScope — ViewModel 销毁时自动取消
class MyViewModel : ViewModel() {
fun loadData() {
viewModelScope.launch {
val data = repository.fetch()
_state.value = data
}
}
}
// lifecycleScope — Activity/Fragment 生命周期绑定
class MyActivity : AppCompatActivity() {
fun loadData() {
lifecycleScope.launch {
val data = repository.fetch()
updateUI(data)
}
}
}
// coroutineScope — 创建临时作用域,所有子协程完成后才结束
suspend fun fetchAll() = coroutineScope {
val user = async { fetchUser() }
val orders = async { fetchOrders() }
Pair(user.await(), orders.await())
}
详细用法
挂起函数
// suspend 修饰的函数可以在协程中调用,也可以调用其他挂起函数
suspend fun fetchUser(): User {
delay(1000) // 模拟网络请求(挂起点)
return User("张三", 25)
}
// 挂起函数是安全的,不会阻塞线程
suspend fun loadData() {
val user = fetchUser() // 挂起,不阻塞
val orders = fetchOrders() // 挂起,不阻塞
updateUI(user, orders)
}
// withContext 切换调度器
suspend fun saveData(data: Data) {
// 在 IO 调度器上执行,完成后切回原调度器
val result = withContext(Dispatchers.IO) {
database.save(data)
}
showResult(result)
}
协程取消与超时
// 取消协程
val job = launch {
repeat(1000) {
// 检查取消状态
ensureActive()
// 或者使用可取消的挂起函数
delay(100)
println("处理: $it")
}
}
delay(500)
job.cancel() // 取消协程
job.join() // 等待取消完成
// 简写:job.cancelAndJoin()
// 超时控制
try {
withTimeout(3000) {
fetchData() // 超过3秒抛出 TimeoutCancellationException
}
} catch (e: TimeoutCancellationException) {
println("请求超时")
}
// 超时返回 null 而非抛异常
val result = withTimeoutOrNull(3000) {
fetchData()
}
if (result == null) {
println("请求超时")
}
异常处理
// SupervisorJob — 子协程失败不影响其他子协程
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
// supervisorScope — 临时作用域
supervisorScope {
launch {
throw RuntimeException("子协程1失败") // 不影响子协程2
}
launch {
delay(100)
println("子协程2正常执行")
}
}
// CoroutineExceptionHandler — 全局异常处理
val handler = CoroutineExceptionHandler { _, exception ->
println("捕获异常: ${exception.message}")
}
scope.launch(handler) {
throw RuntimeException("出错了")
}
常见场景
并发请求
// 使用 async 并发执行多个请求
suspend fun loadPage(): PageData = coroutineScope {
val user = async { apiService.getUser() }
val posts = async { apiService.getPosts() }
val notifications = async { apiService.getNotifications() }
PageData(user.await(), posts.await(), notifications.await())
}
重试机制
// 简单重试
suspend fun <T> retry(
times: Int = 3,
delay: Long = 1000,
block: suspend () -> T
): T {
repeat(times) { attempt ->
try {
return block()
} catch (e: Exception) {
if (attempt == times - 1) throw e
delay(delay * (attempt + 1)) // 递增延迟
}
}
throw IllegalStateException("不应到达此处")
}
// 使用
val data = retry(times = 3) { apiService.fetchData() }
注意事项
- 协程不是线程,协程可以在线程间切换,挂起时不占用线程
- 不要使用 GlobalScope,它创建的协程生命周期与应用相同,容易泄漏
- 挂起函数只能在协程或其他挂起函数中调用
- CPU 密集型任务使用 Dispatchers.Default,I/O 操作使用 Dispatchers.IO
- 协程取消是协作式的,需要在循环或长时间操作中检查 ensureActive()
- 不要在挂起函数中使用 Thread.sleep,应使用 delay
进阶用法
Channel 通信
// 使用 Channel 在协程间传递数据
val channel = Channel<Int>()
// 生产者
launch {
for (i in 1..10) {
channel.send(i)
}
channel.close()
}
// 消费者
launch {
for (item in channel) {
println("收到: $item")
}
}
// produce 构建器
fun CoroutineScope.numberProducer() = produce {
for (i in 1..10) {
send(i)
}
}
Select 等待多个操作
// select 同时等待多个挂起操作,哪个先完成就处理哪个
suspend fun fetchFastest(): String = coroutineScope {
val local = async { cache.get() }
val remote = async { apiService.fetch() }
select {
local.onAwait { "缓存: $it" }
remote.onAwait { "网络: $it" }
}
}