Kotlin - 协程(一) - 协程调度

260 阅读8分钟

协程调度

🌟 协程调度结构

// 想象一个工厂的工作流程:
val scope = CoroutineScope(
    SupervisorJob() +      // 工厂主管
    Dispatchers.IO +       // 工作车间
    CoroutineName("Coil")  // 工人工牌
)

💡 详细解析

// 1. SupervisorJob
class SupervisorExample {
    /* 
    就像工厂主管:
    - 普通Job: 一个工人出错,整条生产线停止
    - SupervisorJob: 一个工人出错,其他工人继续工作
    */
    
    fun example() {
        val supervisor = SupervisorJob()
        
        // 工人1失败不影响工人2
        scope.launch(supervisor) { // 工人1
            throw Exception("工人1出错")
        }
        
        scope.launch(supervisor) { // 工人2
            // 继续工作
        }
    }
}

// 2. Dispatchers.IO
class DispatcherExample {
    /* 
    就像工作车间:
    - Main: 展厅(UI线程)
    - IO: 生产车间(适合IO操作)
    - Default: 计算车间(适合计算密集)
    */
    
    fun example() {
        scope.launch(Dispatchers.IO) {
            // 在IO车间工作
            loadImage() // 加载图片
            saveToCache() // 保存缓存
        }
    }
}

// 3. CoroutineName
class CoroutineNameExample {
    /* 
    就像工人工牌:
    - 方便调试和日志追踪
    - 识别协程来源
    */
    
    fun example() {
        scope.launch(CoroutineName("图片加载工人")) {
            // 工作时能清楚知道是谁在干活
        }
    }
}

⚡ 实际运作示例

// 完整的工作流程
class CoilWorkflow {
    val imageLoader = ImageLoader(context) {
        val scope = CoroutineScope(
            SupervisorJob() +  // 主管监督
            Dispatchers.IO +   // 在IO车间工作
            CoroutineName("Coil-Worker")  // 带工牌
        )
        
        scope.launch {
            // 工人1: 加载图片
            async { loadImage() }
            
            // 工人2: 处理缓存
            async { handleCache() }
            
            // 即使工人1失败
            // 工人2依然继续工作
        }
    }
}

🔄 工作流程图

工厂运作流程:

SupervisorJob (工厂主管)
       ↓
   监督管理
       ↓
Dispatchers.IO (工作车间)
       ↓
   具体工作
       ↓
CoroutineName (工人工牌)

📊 实际应用场景

// 图片加载实例
class ImageLoadingExample {
    fun loadImages() {
        val scope = CoroutineScope(
            SupervisorJob() +  // 主管
            Dispatchers.IO +   // 车间
            CoroutineName("ImageLoader")  // 工牌
        )
        
        scope.launch {
            // 并行加载多张图片
            val images = listOf(
                async { loadImage("1.jpg") },
                async { loadImage("2.jpg") },
                async { loadImage("3.jpg") }
            )
            
            // 即使某张图片加载失败
            // 其他图片仍继续加载
            images.awaitAll()
        }
    }
}

⚠️ 注意事项

class CoroutinePrecautions {
    // 1. 资源管理
    fun cleanup() {
        scope.cancel() // 下班时关闭工厂
    }
    
    // 2. 异常处理
    fun handleErrors() {
        scope.launch {
            try {
                loadImage()
            } catch (e: Exception) {
                // 工人出错时的处理方案
                reportError(e)
            }
        }
    }
}

记住:

  1. SupervisorJob 是宽容的主管
  2. Dispatchers.IO 是专门的工作车间
  3. CoroutineName 是工人的身份标识
  4. 三者配合实现高效工作

协程job

// Job就像是工作任务的"跟踪单"
val job = launch {
    // 任务内容
}

1. 基本功能:

// 1. 检查任务状态
job.isActive    // 任务是否在进行中(像跟踪快递:在途中)
job.isCompleted // 任务是否完成(快递已送达)
job.isCancelled // 任务是否被取消(快递被退回)

// 2. 等待任务完成
job.join()      // 等待任务完成(像等快递送达)

// 3. 取消任务
job.cancel()    // 取消任务(像取消快递订单)

生活中的比喻:

想象Job像餐厅的点餐单:

val orderJob = launch {
    // 准备食物
}

orderJob.isActive    // 厨师正在做
orderJob.isCompleted // 菜已经做好了
orderJob.isCancelled // 顾客取消了订单
orderJob.join()      // 等待上菜
orderJob.cancel()    // 取消订单

2. 父子关系:

val parentJob = launch {
    val childJob1 = launch {
        // 子任务1
    }
    val childJob2 = launch {
        // 子任务2
    }
}

生活中的比喻:

像项目经理分配任务:
- parentJob = 项目总任务
  - childJob1 = 小组A的任务
  - childJob2 = 小组B的任务

如果项目取消(parentJob.cancel()),
所有小组的任务也会取消

3. 异常处理:

val job = launch {
    try {
        // 可能出错的任务
    } catch (e: Exception) {
        // 处理错误
    }
}

生活中的比喻:

像送快递:
- 正常情况:送到目的地
- 异常情况:地址错误、收件人不在
- try-catch就像快递员的应急预案

4. 任务协调:

// 等待多个任务完成
val job1 = launch { task1() }
val job2 = launch { task2() }
val job3 = launch { task3() }

joinAll(job1, job2, job3)  // 等待所有任务完成

生活中的比喻:

像等待多个快递:
- job1 = 衣服快递
- job2 = 食品快递
- job3 = 图书快递
joinAll = 等所有快递都送到

5. 超时处理:

val job = launch {
    withTimeout(5000L) {  // 5秒超时
        // 任务内容
    }
}

生活中的比喻:

像餐厅承诺的出餐时间:
- 如果30分钟内没做好
- 顾客可以取消订单

6. 任务状态转换:

New(新建)→ Active(活动)→ Completed(完成)
                   ↓
              Cancelled(取消)

生活中的比喻:

像快递状态:
待揽收 → 运输中 → 已送达
    ↓
  已取消

使用示例:

// 实际应用场景
coroutineScope.launch {
    val jobs = listOf(
        launch { downloadFile() },
        launch { processData() },
        launch { updateUI() }
    )
    
    // 等待所有任务完成
    jobs.joinAll()
    
    // 或者取消所有任务
    jobs.forEach { it.cancel() }
}

它就像是一个任务的"管理器",可以:

  1. 跟踪任务状态
  2. 控制任务执行
  3. 处理任务异常
  4. 协调多个任务
  5. 管理任务生命周期

协程Flow

让我用生动的例子来讲解Flow,特别是冷流和热流的区别:

1. Flow基础概念:

// Flow就像是一个传送带
val flow = flow {
    emit(1)  // 发送数据
    emit(2)
    emit(3)
}

// 收集数据
flow.collect { value ->
    println(value)
}

生活比喻:

Flow像是点外卖的过程:
- emit 就像厨师做菜
- collect 就像顾客接收外卖
- 不collect(没人点餐)就不会emit(不会做菜)

2. 冷流(Cold Flow):

val coldFlow = flow {
    println("开始准备数据")
    emit("数据1")
    emit("数据2")
    emit("数据3")
}

// 每个collect都会重新执行流
coldFlow.collect { println("第一个收集者: $it") }
coldFlow.collect { println("第二个收集者: $it") }

生活比喻:

冷流像点外卖:
- 每个人点餐,厨师都要重新做一份
- 不点餐就不会做
- 点了才开始做

3. 热流(Hot Flow):

val sharedFlow = MutableSharedFlow<String>()
// 或者
val stateFlow = MutableStateFlow("初始值")

// 多个收集者共享同一个数据流
launch { sharedFlow.collect { println("收集者1: $it") } }
launch { sharedFlow.collect { println("收集者2: $it") } }

// 发送数据
sharedFlow.emit("新数据")  // 两个收集者都会收到

生活比喻:

热流像看直播:
- 主播开始直播(emit数据)
- 所有观众(collectors)同时看到
- 不看也在直播
- 中途进来的观众只能看到之后的内容

4. 对比表格:

特性        冷流(Flow)           热流(SharedFlow/StateFlow)
启动时机    首次收集时           创建后立即开始
数据重放    每次收集都重新执行   多个收集者共享数据
资源消耗    按需执行             持续运行
使用场景    数据库查询、文件读取  实时更新、UI状态

5. 面试重点例子:

// 冷流例子:数据库查询
val userFlow = flow {
    println("执行数据库查询")
    val users = database.queryUsers()
    users.forEach { emit(it) }
}

// 热流例子:UI状态更新
val _uiState = MutableStateFlow(UiState())
val uiState = _uiState.asStateFlow()

// 实时位置更新
val locationFlow = MutableSharedFlow<Location>()

6. 实际应用场景:

// 冷流:文件下载
val downloadFlow = flow {
    val file = downloadFile()
    emit(file)
}

// 热流:聊天消息
val messageFlow = MutableSharedFlow<Message>()

面试回答要点:

  1. 概念对比:
冷流:
- 懒加载(lazy)
- 每个收集者获取完整数据
- 适合一次性数据获取

热流:
- 立即执行
- 多个收集者共享数据
- 适合实时更新场景
  1. 使用场景:
冷流适合:
- 数据库查询
- 文件操作
- 网络请求

热流适合:
- UI状态管理
- 实时数据更新
- 事件总线
  1. 性能考虑:
冷流:
- 按需执行,节省资源
- 适合大量数据处理

热流:
- 持续运行,消耗资源
- 适合实时性要求高的场景

生活类比总结:

冷流像点外卖:
- 点了才做
- 每个人都是新做的
- 适合个性化需求

热流像看直播:
- 持续进行
- 所有人看到一样的
- 适合实时共享

协程中launch与async区别

让我用生动的例子来解释 launchasync 的区别:

1. 返回值不同:

// launch: "发射不管" - 不返回结果
val job = launch {
    println("发射火箭")
    // 返回 Job
}

// async: "等待结果" - 返回 Deferred
val deferred = async {
    println("计算结果")
    42  // 返回 Deferred<Int>
}
val result = deferred.await()  // 获取结果

2. 生活中的例子:

launch 像是快递员

// 派送包裹
launch {
    deliverPackage()  // 送完就完了,不需要等待回执
}

// 类似于:
快递员:"包裹已送到,我走了"
// 不需要等待收件人确认

async 像是外卖员

// 送外卖
val delivery = async {
    deliverFood()  // 需要等待顾客签收
    "送达成功"    // 返回送达状态
}
val status = delivery.await()  // 等待送达确认

// 类似于:
外卖员:"等待顾客确认收货"
// 需要等待顾客确认才能完成订单

3. 实际应用场景:

使用 launch

// 适合"发射不管"的场景
class LoggerService {
    fun logEvent(event: String) {
        scope.launch {
            // 记录日志
            saveLog(event)
            // 不关心结果
        }
    }
}

// 发送通知
fun sendNotification() {
    launch {
        // 发送推送
        pushNotification()
        // 不需要等待结果
    }
}

使用 async

// 需要结果的场景
class ProductViewModel {
    fun loadProduct(id: String) {
        viewModelScope.launch {
            // 并行加载数据
            val product = async { repository.getProduct(id) }
            val reviews = async { repository.getReviews(id) }
            
            // 等待所有数据
            val productInfo = ProductInfo(
                product.await(),  // 需要等待结果
                reviews.await()   // 需要等待结果
            )
            
            // 更新UI
            showProduct(productInfo)
        }
    }
}

4. 形象比喻:

launch 像播放音乐

// 按下播放键就行,不需要返回值
launch {
    playMusic()
}

async 像煮咖啡

// 需要等待咖啡煮好,才能喝
val coffee = async {
    brewCoffee()
}
drinkCoffee(coffee.await())  // 等咖啡煮好才能喝

5. 错误处理:

// launch:适合"发了不管"的场景
launch {
    try {
        riskyOperation()
    } catch (e: Exception) {
        // 处理错误
        handleError(e)
    }
}

// async:需要处理结果的场景
val result = async {
    riskyOperation()
}.await()  // 可能抛出异常

6. 组合使用:

// 在一个协程中组合使用
suspend fun loadData() = coroutineScope {
    // 后台任务,不需要结果
    launch { 
        trackAnalytics() 
    }
    
    // 需要等待结果的任务
    val data = async { 
        fetchData() 
    }
    
    // 使用结果
    processData(data.await())
}

总结:

  1. launch

    • "发射不管"
    • 不返回结果
    • 适合后台任务
    • 返回 Job
  2. async

    • "等待结果"
    • 返回 Deferred
    • 适合需要结果的场景
    • 需要调用 await()

选择建议:

  • 不需要结果用 launch
  • 需要结果用 async
  • 需要并行处理用 async