一、概念
协程必须运行在一个线程上,所以要指定调度器。是一个抽象类,Dispatcher是一个标准库中帮我们封装了切换线程的帮助类,可以调度协程在哪类线程上执行。创建协程时,上下文如果没有指定也没有继承到调度器,则会添加一个默认调度器(调度器通过 ContinuationInterceptor 延续体拦截器实现的)。
二、模式
- 由于子协程会继承父协程的上下文,在父协程上指定调度器模式后子协程默认使用这个模式。
- DEFAULT 针对的是 CPU 密集型计算任务,CPU 核心数是就是最大线程数量,满负荷运行再增加线程数反而降低效率,IO 针对的是(发生在内存之外的)磁盘网卡在做输入输出,此时 CPU 是空闲的,更大的线程数量能充分利用 CPU 调度。
- IO 和 DEFAULT 模式共享同一线程池,重用线程起到优化(DEFAULT 切换到 IO 大概率停留在同一线程上),两者对线程数量限制是独立的不会让对方饥饿。最大限度一起使用的话,默认同时活跃的线程数为 64+CPU数。
- 如果切换的模式和当前模式是相同的,是不会再切线程,而是保持在当前线程继续执行后续代码。但 Main 由于底层采用的是 Handler.post(),因此提供了 immediate。
Dispatcher.Main | 运行于主线程,在 Android 中就是 UI 线程,用来处理一些 UI 交互的轻量级任务。 | 调用 suspend 函数 调用 UI 函数 更新 LiveData |
Dispatcher.Main.immediate | 底层采用的是 Handler.post(),当我们已经处在主线程时再调度到主线程是不必要的开销,此时指定为 immediate 就只会在需要的时候调度,否则直接执行。(ViewModelScope 就处在Android默认的主线程中,因此上下文中的调度器使用了这个) | 函数被withContext包装在Dispatcher.Main上运行时使用。 |
Dispatcher.IO | 运行于线程池,专为IO阻塞型任务进行了优化。最大线程数为64个,只要没超过且没有空闲线程就一直可开辟新线程执行新任务。 | 数据库 文件读写 网络处理 |
Dispatcher.Default | 运行于线程池,专为CPU密集型计算任务进行了优化。最大线程数为CPU核心个数(但不少于2个),若全在忙碌时新任务无法得到执行。 | 数组排序 Json解析 处理差异判断 计算Bitmap |
Dispatcher.Unconfined | 不改变线程,在启动它的线程执行,在恢复它的线程执行,也就是当前线程在哪就在哪执行。调度成本最低性能最好,但存在不可控风险,如处在主线程执行了阻塞操作,实际开发不会用到。 | 当不需要关心协程在哪个线程上被挂起时使用。 |
三、自定义线程数
3.1 限制线程数 limitedParallelism()
1.6版本引入。
- 对于Default模式:当有一个开销很大的任务,可能会导致其它使用相同调度器的协程抢不到线程执行权,这个时候就可以用来限制该协程的线程使用数量。
- 对于IO模式:当有一个开销很大的任务,可能会导致阻塞太多线程让其它任务暂停等待,突破默认64个线程的限制加速执行(不显著)。
- 传参将线程限制为1,解决多线程并发修改数据的同步问题。但如果阻塞了它,其它操作都要等待。
public open fun limitedParallelism(parallelism: Int): CoroutineDispatcher |
suspend fun main(): Unit = coroutineScope {
//使用默认IO模式
launch {
printTime(Dispatchers.IO) //打印:Dispatchers.IO 花费了: 2038/
}
//使用limitedParallelism增加线程
launch {
val dispatcher = Dispatchers.IO.limitedParallelism(100)
printTime(dispatcher) //打印:LimitedDispatcher@1cc12797 花费了: 1037
}
}
suspend fun printTime(dispatcher: CoroutineDispatcher) {
val time = measureTimeMillis {
coroutineScope {
repeat(100) {
launch(dispatcher) {
Thread.sleep(1000)
}
}
}
}
println("$dispatcher 花费了: $time")
}
3.2 专用的单线程/线程池(不推荐)
在没有 limitedParallelism() 以前就是这样做的。专用的线程可能会抵消地使用(未使用的线程保持活跃状态却不与其它业务共享这些线程),使用完容易忘记关闭,创建太多消耗系统资源,容易用完忘记关闭造成内存泄漏。
3.2.1 单线程 newSingleThreadContext
是协程提供的一个用于创建单线程调度器的函数,它可以确保所有在该调度器上执行的协程都在同一个线程中顺序执行。
public fun newSingleThreadContext(name: String): CloseableCoroutineDispatcher = newFixedThreadPoolContext(1, name) |
val singleThreadContext = newSingleThreadContext("SingleThreadContext")
//两个launch不再是并行执行,而是按顺序
launch(singleThreadContext) {...}
launch(singleThreadContext) {...}
singleThreadContext.close()
3.2.2 线程池 newFixedThreadPoolContext
是协程提供的一个用于创建固定大小线程池调度器的函数,当所有线程都忙时新任务会排队等待。
public expect fun newFixedThreadPoolContext(nThreads: Int, name: String): CloseableCoroutineDispatcher |
val threadPoolContext = newFixedThreadPoolContext(3, "ThreadPoolContext")
launch(threadPoolContext) {...}
threadPoolContext.close()
3.3.3 Java线程转换 asCoroutineDispatcher()
将 Java 的方式转为协程版本。
val singleThreadExecutor = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
val fixedThreadPool = Executors.newFixedThreadPool(3).asCoroutineDispatcher()
launch(singleThreadExecutor) {}
五、多线程并发问题
某个协程对共享变量的更新,可能不会立即被其他协程看见,尤其是在多线程环境下,协程可能运行在不同的线程上。多个协程可能会交错执行这些步骤,导致最终值错误。
协程的挂起与恢复可能导致锁资源竞争。如果协程挂起期间未正确释放锁,可能造成死锁。这与我们在使用线程时是一样的
创建10个协程,每个协程执行 i++ 1000次,预期结果 i=10000,实际会小于这个值。i++ 不是线程安全操作,它包含了读取、计算、写入。
//不指定Dispatchers的话,默认是BlockingEventLoop,不是Default
fun main(): Unit = runBlocking(Dispatchers.Default) {
var count = 0
val time = measureTimeMillis {
repeat(10) { //创建10个协程
launch {
repeat(100) { //每个协程执行运算100次
count++
}
}
}
}
print("count = $count, time = $time") //打印:count=800,time=6
}
4.1 避免使用共享变量
fun main(): Unit = runBlocking {
var count = 0
val deferreds = mutableListOf<Deferred<Int>>()
val time = measureTimeMillis {
repeat(10) {
val deferred = async(Dispatchers.Default) {
var i = 0
repeat(1000) { i++ }
return@async i
}
deferreds.add(deferred)
}
deferreds.forEach {
count += it.await()
}
}
print("i = $count, time = $time")
}
//打印:i = 10000,耗时:77
4.2 使用Java方法(不推荐)
可以使用 Synchronized、Lock、Atomic。由于是线程模型下的阻塞方式,不支持调用挂起函数,会影响协程挂起特性。
4.2.1 使用同步锁
fun main() = runBlocking {
val start = System.currentTimeMillis()
var i = 0
val jobs = mutableListOf<Job>()
@Synchronized
fun add() { i++ }
repeat(10) {
val job = launch(Dispatchers.Default) {
repeat(1000) { add() }
}
jobs.add(job)
}
jobs.joinAll()
println("i = $i,耗时:${System.currentTimeMillis() - start}")
}
//打印:i = 10000,耗时:71
4.2.2 使用同步代码块
fun main() = runBlocking {
val start = System.currentTimeMillis()
val lock = Any()
var i = 0
val jobs = mutableListOf<Job>()
repeat(10) {
val job = launch(Dispatchers.Default) {
repeat(1000) {
synchronized(lock) { i++ }
}
}
jobs.add(job)
}
jobs.joinAll()
println("i = $i,耗时:${System.currentTimeMillis() - start}")
}
//打印:i = 10000,耗时:73
4.2.3 使用可重入锁 ReentrantLock
fun main() = runBlocking {
val start = System.currentTimeMillis()
val lock = ReentrantLock()
var i = 0
val jobs = mutableListOf<Job>()
repeat(10) {
val job = launch(Dispatchers.Default) {
repeat(1000) {
lock.lock()
i++
lock.unlock()
}
}
jobs.add(job)
}
jobs.joinAll()
println("i = $i,耗时:${System.currentTimeMillis() - start}")
}
//打印:i = 10000,耗时:83
4.2.4 使用 Atomic 保证原子性
fun main() = runBlocking {
val start = System.currentTimeMillis()
var i = AtomicInteger(0)
val jobs = mutableListOf<Job>()
repeat(10) {
val job = launch(Dispatchers.Default) {
repeat(1000) { i.incrementAndGet() }
}
jobs.add(job)
}
jobs.joinAll()
println("i = $i,耗时:${System.currentTimeMillis() - start}")
}
//打印:i = 10000,耗时:89
4.3 使用单线程(不推荐)
fun main() = runBlocking {
val start = System.currentTimeMillis()
val mySingleDispatcher = Executors.newSingleThreadExecutor {
Thread(it, "我的线程").apply { isDaemon = true }
}.asCoroutineDispatcher()
var i = 0
val jobs = mutableListOf<Job>()
repeat(10) {
val job = launch(mySingleDispatcher) {
repeat(1000) { i++ }
}
jobs.add(job)
}
jobs.joinAll()
println("i = $i,耗时:${System.currentTimeMillis() - start}")
mySingleDispatcher.close() //用完容易忘记关闭
}
//打印:i = 10000,耗时:64
4.4 使用 Mutex
Java方式不支持调用挂起函数,同步锁是阻塞式的会影响协程特性,为此 Kotlin 提供了非阻塞式锁Mutex。使用 mutex.lock() 和 mutex.unlock() 包裹需要同步的计算逻辑就可以实现多线程同步了,但由于包裹内容可能出现的异常使得 unlock() 无法被执行,写在 finally{} 中会很繁琐,因此提供了扩展函数 mutex.withLock{ },本质就是在 finally{ } 中调用了 unlock()。
public suspend inline fun <T> Mutex.withLock(owner: Any? = null, action: () -> T): T { lock(owner) try { return action() } finally { // 注意,这里并没有 catch 代码块,所以不会捕获异常 unlock(owner) } } |
fun main() = runBlocking {
val start = System.currentTimeMillis()
var i = 0
val mutex = Mutex()
//使用方式一
mutex.lock()
// try {
// repeat(10000) { i++ }
// } catch (e: Exception) {
// e.printStackTrace()
// } finally {
// mutex.unlock()
// }
//使用方式二
mutex.withLock {
try {
repeat(10000) { i++ }
} catch (e: Exception) {
e.printStackTrace()
}
}
println("i = $i,耗时:${System.currentTimeMillis() - start}")
}
//方式一打印:i = 10000,耗时:17
//方式二打印:i = 10000,耗时:17
4.5 使用 Channel 的 actor()
Channel 是并发安全的,生产和消费交替进行。actor() 创建 SendChannel 定义执行逻辑,往流中发送一次消息就执行一次。
sealed class Msg {
object AddMsg : Msg()
class ResultMsg(val result: CompletableDeferred<Int>) : Msg()
}
@OptIn(ObsoleteCoroutinesApi::class)
fun main() = runBlocking {
val start = System.currentTimeMillis()
val actor = actor<Msg> {
var i = 0
for (msg in channel) {
when (msg) {
is Msg.AddMsg -> i++
is Msg.ResultMsg -> msg.result.complete(i)
}
}
}
val jobs = mutableListOf<Job>()
repeat(10) {
val job = launch {
repeat(1000) {
actor.send(Msg.AddMsg)
}
}
jobs.add(job)
}
jobs.joinAll()
val deferred = CompletableDeferred<Int>()
actor.send(Msg.ResultMsg(deferred))
val result = deferred.await()
actor.close()
println("i = $result,耗时:${System.currentTimeMillis() - start}")
}
//打印:i = 10000,耗时:167
4.6 使用 Semaphore
用于限制同时访问某个资源的协程数量。
fun main() = runBlocking {
var count = 0
val semaphore = Semaphore(1) //只允许一个协程访问
repeat(1000) {
GlobalScope.launch {
semaphore.withPermit { count++ }
}
}.joinAll()
println(count)
}
五、第三方库调用的线程问题
使用第三方库的时候(Retrofit网络请求、Room查询数据库),它们已经在内部处理好了IO调度,用它们自己的线程来管理,因此不要用 withContext(Dispatcher.IO) 去包裹,你只是把诸如准备请求和解析 json 这类占用 cpu 的工作交给了 IO 调度器,反而画蛇添足。
IO任务是一种不会给CPU带来高负载的工作,大部分时间都消耗在等待硬件响应上(持久化存储或远程主机)。CPU密集型任务主要靠CPU运算,默认的 Dispatcher.Default 线程数和CPU核心数相对应,能充分利用CPU核心避免过多的线程频繁切换上下文增加额外开销,本来一个线程就能高效完成结果 Dispatcher.IO 的64个线程抢着做。