Android 协程并发控制:别动线程池,控制好并发语义就够了

最近 review 代码,又看到一个很熟悉的写法:

kotlin 复制代码
val executor = Executors.newFixedThreadPool(4)
val dispatcher = executor.asCoroutineDispatcher()

问了一下原因:"任务有点多,怕一下子跑太猛,所以限制一下并发。"

这个思路本身没问题,只是它更像 Java 时代的条件反射------把"控制并发"直接等价成"控制线程数"。

但在 Kotlin 协程里,这个等价关系其实已经不成立了。


一、协程本身已经有一套调度系统

Kotlin 协程默认就有两套调度器:

  • Dispatchers.Default:CPU 密集任务(共享线程池)
  • Dispatchers.IO:IO 阻塞任务(弹性线程池)

它们有一个关键特点:全局共享

你写的每一个 launch {},本质上都是进入同一个调度体系排队执行,而不是自己占一套资源。

所以协程的默认假设其实是:

线程池是运行时基础设施,不是业务控制手段。

一旦你在每个模块里都 newFixedThreadPool,这个假设就被打破了。

系统很快会变成这样:

sql 复制代码
Default pool
IO pool
Module A pool
Module B pool
SDK pool
...

看起来"更可控",但实际上是把统一调度拆碎了。


二、"限流"和"隔离"不是一件事儿

这里是关键点。

很多人用线程池,其实想解决的是:

"最多同时跑 4 个请求"

但线程池表达的是:

  • 用多少线程执行
  • 任务在哪执行
  • 是否和其他模块隔离资源

这两个维度不是一回事。

1. 语义被埋进了线程配置里

kotlin 复制代码
newFixedThreadPool(4)

看起来是在做限流,但实际上表达的是:

"这里有 4 条线程"

问题是,线程数 ≠ 并发语义。

当需求变成:

  • 从"4 并发"变成"串行"
  • 或"新任务覆盖旧任务"

线程池这个模型就开始不够用了。


2. 共享调度能力被破坏

协程的优势之一是:

线程可以复用,全局调度统一管理

但一旦拆线程池,就变成:

  • A 模块一套线程
  • B 模块一套线程
  • SDK 再来一套

结果是:

  • 线程利用率下降
  • 空闲资源无法共享
  • 调度变成碎片化

所谓"更可控",很多时候只是"更分裂"。


3. 排查问题会变复杂

出了问题之后你会看到:

  • IO pool 在忙
  • A pool 在排队
  • B pool 卡住了

但真正难的是回答:

到底是并发太高,还是资源分配不合理?

如果用的是协程原语(Semaphore / Channel / limitedParallelism),你看到的是:

  • 并发上限是多少
  • 有没有排队
  • 哪个环节被限流

这些信息是"业务语义级"的。

而线程池暴露的是"执行细节"。


三、一个更合理的折中:limitedParallelism

如果只是想"限制某类任务并发",其实可以用:

kotlin 复制代码
val networkDispatcher = Dispatchers.IO.limitedParallelism(10)

或者:

kotlin 复制代码
val imageDispatcher = Dispatchers.Default.limitedParallelism(2)

它的特点很直接:

  • 不创建新线程
  • 复用全局调度池
  • 但限制并发上限

适合大多数"怕打爆系统"的场景。


四、真正表达并发语义的方式

1. Semaphore:限制并发数

kotlin 复制代码
val semaphore = Semaphore(4)

suspend fun loadData() {
    semaphore.withPermit {
        api.request()
    }
}

表达的是:

同时最多 4 个任务在执行


2. Channel / Flow:任务排队

kotlin 复制代码
val channel = Channel<Task>()

launch {
    for (task in channel) {
        handle(task)
    }
}

表达的是:

所有任务按队列串行消费


3. Structured Concurrency:控制生命周期

kotlin 复制代码
coroutineScope {
    val a = async { fetchA() }
    val b = async { fetchB() }
}

表达的是:

这一组任务是一个整体,要么一起成功,要么一起取消


五、什么时候新线程池还是合理的

线程池不是不能用,只是用途变了。

比较合理的场景:

  • CPU 密集型任务(图像、加密、AIGC)
  • 第三方 SDK 阻塞调用(不可控)
  • 音视频 pipeline 这种长任务流,以及下载任务

这些本质是在做:

资源隔离,而不是并发控制


六、简单的分层理解

可以这么拆:

bash 复制代码
UI 层:管生命周期(viewModelScope)
Domain 层:管并发语义(Semaphore / async)
Data 层:只负责数据(suspend / Flow)

Data 层不应该知道"并发是多少",它只负责"给数据"。


七、对比一下几种方式

方式 本质 推荐
自建线程池 资源隔离 ❌ 慎用
limitedParallelism 调度限制
Semaphore 并发语义
Channel / Flow 数据流
structured concurrency 生命周期

八、总结

线程池解决的是:

"任务在哪里跑"

而协程里的并发控制解决的是:

"允许多少任务同时发生"

很多问题之所以变复杂,就是因为把"并发语义"错误地降级成了"线程配置"。

在协程体系里,更自然的做法是:

  • limitedParallelism 控制资源上限
  • Semaphore 表达并发规则
  • Channel / Flow 表达任务流
  • 用 structured concurrency 管生命周期

而不是一遇到问题就:

"要不要先新建个线程池稳一下?"

相关推荐
weiggle18 小时前
第七篇:状态提升与单向数据流——架构设计的核心
android
xingpanvip18 小时前
星盘接口开发文档:本命盘接口指南
android·开发语言·css·php·lua
goldenrolan18 小时前
A公司物料替代测试系统 v1.7:从需求到 exe/apk 的 AI 辅助全链路实践
android·自动化测试·软件测试·python·ai
AC赳赳老秦19 小时前
用 OpenClaw 搭建服务器故障应急响应系统,自动处理 80% 常见运维故障
android·运维·服务器·python·rxjava·deepseek·openclaw
骇客之技术20 小时前
AutoLua:在安卓上写 Lua 脚本
android·junit·lua
kiros_wang21 小时前
Android 常见面试题
android
货拉拉技术21 小时前
Hook植入日志协助定位问题方案
android
FlightYe1 天前
Android投屏MirrorCast全链路
android
Ehtan_Zheng1 天前
Kotlin const val vs val:字节码、性能与隐藏陷阱详解
android·kotlin