【Kotlin 协程修仙录 · 渡劫境 · 中阶】 | 造化神兵:自定义 CoroutineDispatcher 与调度器的终极定制

前言

渡劫初成,你已看透 CPS 变换与状态机的字节码真身。COROUTINE_SUSPENDED 的信使身份在你眼中不再神秘,BaseContinuationImpl 的恢复循环如掌上观纹。你对协程的理解,已入化境。

然而,真正的造物主,从不满足于使用现成的法器。当标准库的 Dispatchers 无法满足你的特殊需求时,你是否渴望亲手锻造属于自己的调度神兵?

"我有一个对延迟极度敏感的任务,希望能优先执行 ,而不是在 Dispatchers.IO 的队列里排队。" "我需要一个令牌桶限流器 ,让协程按照固定速率执行,避免打爆下游服务。" "我的任务必须运行在指定的线程上,因为那个线程绑定了特殊的 OpenGL 上下文或 JNI 资源。"

这些需求,标准的 Dispatchers 无法直接满足。但协程的设计者早已为你留下了造物之门 ------CoroutineDispatcher 的抽象接口。只要你理解其内部的 dispatchlimitedParallelism 机制,就能锻造出任意特性的调度神兵。

本讲是渡劫境的中阶修炼,也是整个修仙系列的倒数第二讲。你将:

  • 彻底掌握 CoroutineDispatcher 的定制方法。
  • 亲手实现一个优先级任务队列调度器
  • 锻造一个令牌桶限流调度器,控制协程执行速率。
  • 创建线程亲和性调度器,将任务绑定到特定线程。
  • 将这些定制调度器与 limitedParallelism 结合,构建工业级并发控制。

准备好执掌造化,锻造属于你的调度神兵了吗?我们开始。

千曲 而后晓声,观千剑 而后识器。虐它千百遍 方能通晓其真意


自定义 CoroutineDispatcher 的核心接口

需要重写的三个方法

kotlin 复制代码
abstract class CoroutineDispatcher : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
    abstract fun dispatch(context: CoroutineContext, block: Runnable)
    open fun isDispatchNeeded(context: CoroutineContext): Boolean = true
    open fun limitedParallelism(parallelism: Int): CoroutineDispatcher = this
}
方法 作用 默认行为
dispatch 将任务 Runnable 提交给底层执行器 必须实现
isDispatchNeeded 判断是否需要调度(若当前已在目标线程,可返回 false 优化) 返回 true
limitedParallelism 返回一个限制并发数的调度器视图 返回自身

一个最小化的自定义调度器

kotlin 复制代码
import kotlinx.coroutines.CoroutineDispatcher
import java.util.concurrent.Executors
import kotlin.coroutines.CoroutineContext

class SingleThreadDispatcher : CoroutineDispatcher() {
    private val executor = Executors.newSingleThreadExecutor { r ->
        Thread(r, "MySingleThread")
    }

    override fun dispatch(context: CoroutineContext, block: Runnable) {
        executor.execute(block)
    }

    fun close() {
        executor.shutdown()
    }
}

这个调度器将所有任务串行 执行在一个名为 MySingleThread 的线程上。这是所有自定义调度器的起点。

flowchart LR subgraph Coroutine[协程] L[launch] end subgraph Dispatcher[SingleThreadDispatcher] D[dispatch] E[Executor 单线程池] end subgraph Worker[工作线程] W[MySingleThread] end L --> D --> E --> W style Coroutine fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px style Dispatcher fill:#fff3e0,stroke:#f57c00,stroke-width:2px style Worker fill:#e3f2fd,stroke:#1976d2,stroke-width:2px style L fill:#c8e6c9 style D fill:#ffb74d style E fill:#ffb74d style W fill:#90caf9

优先级调度器:让 VIP 任务插队

设计思路

我们需要一个支持优先级的任务队列。Java 标准库提供了 PriorityBlockingQueue,但它是阻塞 队列。协程调度器的 dispatch 方法应非阻塞 ,因此我们使用 PriorityBlockingQueue 配合一个 Worker 线程不断 take 任务执行。

kotlin 复制代码
import kotlinx.coroutines.CoroutineDispatcher
import java.util.concurrent.PriorityBlockingQueue
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.CoroutineContext

class PriorityDispatcher : CoroutineDispatcher() {
    // 优先级任务包装类
    private data class PrioritizedTask(
        val priority: Int,      // 数字越小优先级越高
        val block: Runnable
    ) : Comparable<PrioritizedTask> {
        override fun compareTo(other: PrioritizedTask): Int = 
            priority.compareTo(other.priority)
    }

    private val queue = PriorityBlockingQueue<PrioritizedTask>()
    private val worker = Thread {
        while (!Thread.interrupted()) {
            val task = queue.take()
            task.block.run()
        }
    }.apply {
        name = "PriorityDispatcher-Worker"
        start()
    }

    fun dispatch(priority: Int, block: Runnable) {
        queue.put(PrioritizedTask(priority, block))
    }

    override fun dispatch(context: CoroutineContext, block: Runnable) {
        dispatch(Int.MAX_VALUE, block) // 默认最低优先级
    }

    fun close() {
        worker.interrupt()
    }
}

使用示例

kotlin 复制代码
fun main() = runBlocking {
    val dispatcher = PriorityDispatcher()
    val scope = CoroutineScope(dispatcher)
    
    // 低优先级任务
    scope.launch {
        println("低优先级任务执行")
    }
    
    // 手动提交高优先级任务(通过扩展函数)
    fun CoroutineScope.launchWithPriority(priority: Int, block: suspend () -> Unit) {
        launch {
            (coroutineContext[ContinuationInterceptor] as? PriorityDispatcher)
                ?.dispatch(priority) { 
                    // 注意:这里需要手动处理挂起,简化起见仅演示
                    runBlocking { block() } 
                }
        }
    }
    
    scope.launchWithPriority(1) {
        println("高优先级任务先执行")
    }
    
    delay(1000)
    dispatcher.close()
}
flowchart LR subgraph Submit[任务提交] H[高优先级任务] --> Q[PriorityBlockingQueue] L[低优先级任务] --> Q end subgraph Worker[Worker 线程] W[不断 take] Q --> W W --> Run[执行任务] end Q -.->|按优先级排序| W style Submit fill:#e3f2fd,stroke:#1976d2,stroke-width:2px style Worker fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px style Q fill:#ffb74d style W fill:#a5d6a7

令牌桶限流调度器:控制执行速率

令牌桶算法简介

令牌桶 是经典的限流算法:系统以固定速率向桶中放入令牌,每个任务执行前需获取一个令牌。若桶中无令牌,任务需等待。

我们可以利用 ChannelRENDEZVOUSBUFFERED 特性来模拟令牌桶。

kotlin 复制代码
import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext

class TokenBucketDispatcher(
    private val permitsPerSecond: Int,      // 每秒产生的令牌数
    private val bucketCapacity: Int = permitsPerSecond // 桶容量
) : CoroutineDispatcher() {
    // 使用 Channel 作为令牌桶,容量为 bucketCapacity
    private val tokenChannel = Channel<Unit>(bucketCapacity)
    
    init {
        // 启动令牌生产者协程
        GlobalScope.launch {
            val intervalMs = 1000L / permitsPerSecond
            while (true) {
                tokenChannel.trySend(Unit)
                delay(intervalMs)
            }
        }
    }

    override fun dispatch(context: CoroutineContext, block: Runnable) {
        // 在调度器的上下文中启动一个协程来等待令牌
        // 注意:这里简化了实现,实际需要更严谨的线程管理
        GlobalScope.launch {
            tokenChannel.receive() // 获取令牌,若没有则挂起
            block.run()
        }
    }
}

更严谨的实现 :我们可以将任务提交给一个单线程执行器,并在执行前等待令牌。由于 dispatch 必须立即返回,我们可以在 Runnablerun 方法中内置令牌等待逻辑。

kotlin 复制代码
class TokenBucketDispatcher(
    private val rate: Int,
    private val capacity: Int = rate
) : CoroutineDispatcher() {
    private val semaphore = Semaphore(capacity)
    
    init {
        // 定时释放许可
        GlobalScope.launch {
            val interval = 1000L / rate
            while (true) {
                repeat(rate) {
                    semaphore.tryAcquire() // 尝试清理多余许可
                }
                semaphore.release(rate.coerceAtMost(capacity - semaphore.availablePermits()))
                delay(interval)
            }
        }
    }

    override fun dispatch(context: CoroutineContext, block: Runnable) {
        GlobalScope.launch {
            semaphore.acquire()
            block.run()
        }
    }
}

线程亲和性调度器:绑定任务到指定线程

场景:OpenGL 渲染必须在 GL 线程

Android 的 GLSurfaceView 要求所有 OpenGL 调用必须在同一个线程(GL 线程)上执行。我们可以自定义一个调度器,确保任务始终在某个特定线程上运行。

kotlin 复制代码
import kotlinx.coroutines.CoroutineDispatcher
import java.util.concurrent.LinkedBlockingQueue
import kotlin.coroutines.CoroutineContext

class AffinityDispatcher(private val targetThread: Thread) : CoroutineDispatcher() {
    private val queue = LinkedBlockingQueue<Runnable>()
    
    // 如果当前已经在目标线程,则无需调度(优化)
    override fun isDispatchNeeded(context: CoroutineContext): Boolean {
        return Thread.currentThread() != targetThread
    }
    
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        if (Thread.currentThread() == targetThread) {
            block.run()
        } else {
            queue.put(block)
            // 唤醒目标线程(如果它正在等待)
            synchronized(targetThread) {
                targetThread.notify()
            }
        }
    }
    
    // 目标线程需要不断消费队列中的任务
    fun processQueue() {
        while (!Thread.interrupted()) {
            val task = try {
                queue.take()
            } catch (e: InterruptedException) {
                return
            }
            task.run()
        }
    }
}

使用方式

kotlin 复制代码
val glThread = Thread {
    val dispatcher = AffinityDispatcher(Thread.currentThread())
    val scope = CoroutineScope(dispatcher)
    
    scope.launch {
        // 所有在此 scope 中启动的协程都会运行在 glThread 上
        glRender()
    }
    
    dispatcher.processQueue() // 开始处理队列
}
glThread.start()
flowchart LR subgraph Caller[调用方线程] C[协程 launch] end subgraph Dispatcher[AffinityDispatcher] D[dispatch] Q[任务队列] end subgraph Target[目标线程] T[Worker 循环] end C --> D D -->|当前非目标线程| Q Q --> T T --> Run[执行任务] D -->|当前已是目标线程| Run style Caller fill:#e3f2fd,stroke:#1976d2,stroke-width:2px style Dispatcher fill:#fff3e0,stroke:#f57c00,stroke-width:2px style Target fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px style Q fill:#ffb74d

limitedParallelism 结合:构建工业级调度器

limitedParallelismCoroutineDispatcher 的扩展函数,它可以基于任何调度器创建一个并发限制视图。结合我们自定义的调度器,可以构建出既有特殊调度策略(如优先级),又能限制最大并发的强大法器。

kotlin 复制代码
val priorityDispatcher = PriorityDispatcher()
val limitedPriorityDispatcher = priorityDispatcher.limitedParallelism(4)

// 现在,最多只有 4 个协程能同时执行,且按优先级排序
val scope = CoroutineScope(limitedPriorityDispatcher)

内部原理limitedParallelism 返回一个 LimitedDispatcher,它内部维护一个信号量。任务提交时先获取信号量,然后才转发给原始调度器。

flowchart LR subgraph Coroutine[协程] L[launch] end subgraph Limited[LimitedDispatcher] S[Semaphore permits=N] Q[等待队列] end subgraph Original[原始调度器 PriorityDispatcher] D[dispatch] end L --> S S -->|获取许可| D S -->|无许可| Q Q -->|释放许可后| D style Coroutine fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px style Limited fill:#fff3e0,stroke:#f57c00,stroke-width:2px style Original fill:#e3f2fd,stroke:#1976d2,stroke-width:2px style S fill:#ffb74d

实战:用自定义调度器优化图片上传服务

场景:你有一个图片上传服务,需要:

  1. 最多同时上传 3 张图片(防止占用过多带宽)。
  2. 用户可以标记某张图片为"紧急",紧急图片优先上传。
  3. 上传速率需控制在每秒不超过 10 个请求(令牌桶限流)。
kotlin 复制代码
class ImageUploadService {
    // 优先级调度器 + 并发限制 = 最多 3 个并发,且按优先级排序
    private val priorityDispatcher = PriorityDispatcher()
    private val uploadDispatcher = priorityDispatcher.limitedParallelism(3)
    
    // 令牌桶限流器:每秒 10 个令牌
    private val rateLimiter = TokenBucketDispatcher(rate = 10, capacity = 10)
    
    suspend fun uploadImage(image: File, isUrgent: Boolean = false) {
        val priority = if (isUrgent) 1 else 100
        withContext(uploadDispatcher) {
            // 手动提交到优先级调度器(简化演示)
            // 实际封装后可通过扩展函数优雅调用
        }
        withContext(rateLimiter) {
            // 真正执行上传,受令牌桶限流
            performUpload(image)
        }
    }
    
    private suspend fun performUpload(image: File) {
        delay(500) // 模拟上传
        println("上传完成:${image.name}")
    }
}
flowchart TD subgraph Request[上传请求] U1[普通图片] --> P1[优先级 100] U2[紧急图片] --> P2[优先级 1] end subgraph Priority[优先级调度器 + 并发限制=3] PQ[优先级队列] Worker[Worker 线程] end subgraph RateLimiter[令牌桶限流器 10/s] TB[令牌桶] end subgraph Upload[实际上传] Up[performUpload] end P1 --> PQ P2 --> PQ PQ --> Worker Worker --> TB TB --> Up style Request fill:#e3f2fd,stroke:#1976d2,stroke-width:2px style Priority fill:#fff3e0,stroke:#f57c00,stroke-width:2px style RateLimiter fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px style Upload fill:#e8f5e9,stroke:#388e3c style PQ fill:#ffb74d style TB fill:#a5d6a7

常见错误与避坑指南

错误 1:在 dispatch 中执行耗时操作

kotlin 复制代码
override fun dispatch(context: CoroutineContext, block: Runnable) {
    Thread.sleep(100) // 严重错误!阻塞了调用线程
    executor.execute(block)
}

dispatch 可能被协程调度器的线程调用,必须立即返回

错误 2:忘记关闭自定义调度器的线程池

kotlin 复制代码
val dispatcher = SingleThreadDispatcher()
// 使用后忘记 close(),线程残留

务必在适当时机(如 onCleared)调用 close() 释放资源。

错误 3:在 isDispatchNeeded 中返回错误的值

kotlin 复制代码
override fun isDispatchNeeded(context: CoroutineContext) = false

如果返回 false,任务将在当前线程同步执行,可能阻塞 UI 或破坏并发预期。


最佳实践

  1. 优先考虑组合而非继承 :使用 limitedParallelism 和现有调度器组合出新特性。
  2. 为自定义调度器提供 close 方法:释放线程池资源。
  3. isDispatchNeeded 中做性能优化:当任务已在目标线程时避免不必要的队列操作。
  4. 使用 asCoroutineDispatcher 扩展函数 :可将 ExecutorService 直接转换为 CoroutineDispatcher
  5. 测试时使用 TestCoroutineDispatcher 验证自定义调度器的行为

总结与下回预告

恭喜,你已执掌造化,能亲手锻造任意特性的调度神兵!渡劫境中阶修炼完成!

本讲核心收获

  • 自定义 CoroutineDispatcher 只需实现 dispatch 方法。
  • 优先级调度器通过 PriorityBlockingQueue 实现任务排序。
  • 令牌桶限流调度器利用 SemaphoreChannel 控制速率。
  • 线程亲和调度器将任务绑定到指定线程执行。
  • 结合 limitedParallelism 可构建复杂工业级调度器。

在下一讲------渡劫境·后阶 ,也是本修仙系列的最终章 ------中,我们将汇聚九境全部所学,从零构建一个工业级协程网络请求框架 ,涵盖:自定义调度器、Flow 重试、Channel 任务队列、StateFlow UI 状态、异常处理、单元测试。届时你将真正飞升,成为协程世界的剑仙。


【当前境界修为面板】

当前境界 修炼技能 修炼进度 修炼心得
渡劫境 · 中阶 1、优先级调度诀 2、令牌桶限流术 3、线程亲和调度法 当前进度70% 修为700/1000 下一突破[渡劫境 · 后阶] (需领悟:整合九境,构建工业级协程网络框架) 掌握了dispatch方法,你就掌握了协程调度的终极控制权。造自己的调度神兵。

【本讲思考题】

  1. 表象题limitedParallelism 内部是如何实现并发数限制的?它使用了什么同步机制?

  2. 场景题 :你需要设计一个"任务依赖调度器":任务 B 必须在任务 A 完成后才能执行。如何在自定义 CoroutineDispatcher 中实现?

  3. 原理题CoroutineDispatcherisDispatchNeeded 默认返回 true。如果我们将优先级调度器的 isDispatchNeeded 改为始终返回 false,会发生什么?为什么?


道友,飞升前的最后一道关隘已在眼前。下一讲,我们将汇聚九境修为,铸就无上神兵。渡劫境·后阶见。

欢迎一键四连关注 + 点赞 + 收藏 + 评论

相关推荐
pengyu1 小时前
【Kotlin 协程修仙录 · 渡劫境 · 初阶】 | 飞升雷劫:CPS 变换与挂起函数字节码终极透视
android·kotlin
TimeFine1 小时前
智能眼镜开发:眼镜Touch后收音与触发播放系统音乐的矛盾处理
android
TimeFine2 小时前
智能眼镜开发:眼镜侧收集音频
android
又见情义2 小时前
Android 系统设置从平板版迁移至TV版实践
android
杉氧5 小时前
打破边界(一):实战编写 Android/iOS 原生模块 (Native Modules)
android·react native·前端框架
baidu_247438616 小时前
Android 35适配
android
mmsx7 小时前
osmdroid 踩坑清单:大级别崩溃/路径消失/低内存卡顿/范围缩放不准
android·源码·地图·osmdroid
gf13211117 小时前
【python_回复邮件】
android·java·python