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

46 阅读9分钟

image_11.png

前言

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

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

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

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

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

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

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

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


自定义 CoroutineDispatcher 的核心接口

需要重写的三个方法

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返回一个限制并发数的调度器视图返回自身

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

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 任务执行。

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()
    }
}

使用示例

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 特性来模拟令牌桶。

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 方法中内置令牌等待逻辑。

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()
        }
    }
}

deepseek_mermaid_20260904_70f244.png


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

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

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

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()
        }
    }
}

使用方式

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

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 个请求(令牌桶限流)。
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 中执行耗时操作

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

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

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

val dispatcher = SingleThreadDispatcher()
// 使用后忘记 close(),线程残留

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

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

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,会发生什么?为什么?


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

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