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

前言

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

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

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

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

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

  • 彻底掌握 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

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

令牌桶算法简介

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

我们可以利用 Channel 的 RENDEZVOUS 或 BUFFERED 特性来模拟令牌桶。

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 必须立即返回,我们可以在 Runnable 的 run 方法中内置令牌等待逻辑。

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 结合:构建工业级调度器

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

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 实现任务排序。
  • 令牌桶限流调度器利用 Semaphore 或 Channel 控制速率。
  • 线程亲和调度器将任务绑定到指定线程执行。
  • 结合 limitedParallelism 可构建复杂工业级调度器。

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


【当前境界修为面板】

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

【本讲思考题】

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

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

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


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

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

相关推荐
千里马学框架3 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台3 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone3 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc3 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo3 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077003 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
ai2work3 天前
ch23 综合复刻:从零做一个最小可用版本(capstone)
kotlin
其实防守也摸鱼3 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
ai2work3 天前
ch21 签名、校验与发版
kotlin
AFinalStone3 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui