协程中的线程

130 阅读3分钟

default 与 io 有可能是同一个线程

协程内部有一套自己的线程池,而且没有强制指定某个线程只能运行 default 或 io 任务,同一个线程有可能前一次运行 default 任务,后一次就运行 io 任务。所以 运行 default 与 io 任务的线程有可能是同一个

如下代码,launch1 与 launch2 输出的线程 id 有可能一样

val j = viewModelScope.launch {
    launch(Dispatchers.Default) {
        e("launch1 ${System.identityHashCode(Thread.currentThread())}")
        launch(Dispatchers.IO) {
            e("launch2 ${System.identityHashCode(Thread.currentThread())}")
        }
   

协程线程池中不限制线程数量

协程的线程池是一个不限量的线程池,但我们的任务还是会出现排队的情况。这是因为默认情况下,default 与 io 会限制提交至线程池中的任务数,但此种限制可通过 limitedParallelism 绕过

如下最终会创建 300 个线程,即 set.size = 300

val set = HashSet<Int>()
viewModelScope.launch(Dispatchers.IO.limitedParallelism(200)) {
    repeat(200) {
        launch {
            e("1 index = $it")
            set.add(System.identityHashCode(Thread.currentThread()))
            Thread.sleep(2000)
        }
    }
}
viewModelScope.launch(Dispatchers.IO.limitedParallelism(100)) {
    repeat(100) {
        launch {
            e("index = $it")
            set.add(System.identityHashCode(Thread.currentThread()))
            Thread.sleep(2000)
        }
    }
}

limitedParallelism() 的作用

上面说过在将任务提交给线程池前会对任务数量做限制,该方法的作用就是绕过任务数量限制,即每一次调用该方法就会生成一个新的任务队列,在队列未满时会不断地往线程池中添加任务,与其它队列是否有任务、有多少任务无关。

但要注意该方法只对 IO 有效,对 default 无论指定成多大的值都会被协程内部限制

例如如上例同样的代码,将 IO 换成 Default,则 set.size 不可能是 300,而是 Default 对应的最大并发量

挂起函数前后的线程有可能不是同一个

即使在同一个协程块中,挂起前和恢复后的线程有可能不是同一个,这取决于恢复的代码是在哪个线程中调用的。如下程序可验证该结论

viewModelScope.launch(Dispatchers.IO) {
    repeat(200) {
        val id = System.identityHashCode(Thread.currentThread())
        delay(100)
        val id2 = System.identityHashCode(Thread.currentThread())
        if(id != id2){
            e("id = $id, id2 = $id2")
        }
    }
}

同一个调度器内再次使用,有可能不是同一个线程

在同一个调度器内部再次使用该调度器启动协程,新协程也有可能运行在另一个线程中。简单理解,使用 launch 相当于向线程池中提交了一个任务,该任务最终会被执行在哪个线程是无法确定的。

如下程序可验证该结论

repeat(200) {
    viewModelScope.launch {
        // 紧挨着同时使用 IO,但线程依旧有可能不同
        launch(Dispatchers.IO) {
            val id = System.identityHashCode(Thread.currentThread())
            launch(Dispatchers.IO) {
                val id2 = System.identityHashCode(Thread.currentThread())
                if (id != id2) {
                    e("id = $id, id2 = $id2")
                }
            }
        }
    }
}

withContext 中使用同一调度器,不会切线程

withContext 的作用有二:

  1. 可以返回 lambda 表达式的结果
  2. 上文使用同一调度器时,不会切线程。如下程序可验证,一万次循环中 id 与 id2 始终是同一线程
  3. withContext 中使用挂起函数后恢复,前后的线程不一定是同一个,但恢复后的代码不一定运行在恢复线程中,这是与 Dispatchers.Unconfined 的一个区别。

// 结论 2 验证程序
viewModelScope.launch {
    for (i in 0..10000) {
        withContext(Dispatchers.IO) {
            val id = System.identityHashCode(Thread.currentThread())
            withContext(Dispatchers.IO) {
                val id2 = System.identityHashCode(Thread.currentThread())
                if (id != id2) {
                    e("id $id  id2 $id2")
                }
            }
        }
    }
}

// 结论 3 验证程序
// 虽然 before 与 end 不一定是同一个线程,但一定是同一个线程组
// 所以下面程序有可能输出三个不同的线程
withContext(Dispatchers.IO) {
    e("before = ${Thread.currentThread().name}")
    withContext(Executors.newSingleThreadExecutor().asCoroutineDispatcher()) {
        e("second ${Thread.currentThread().name}")
    }
    e("end = ${Thread.currentThread().name}")
}
// 可能的输出,三个输出三个线程
before = DefaultDispatcher-worker-3
second pool-11-thread-1
end = DefaultDispatcher-worker-2

Dispatchers.Unconfined 不会切线程

前面的代码运行在哪个线程,后面的代码就运行在哪个线程,与是不是协程无关。它有两层含义:

  1. 它会在当前线程中执行协程
  2. 挂起函数返回时会直接运行在恢复的线程中,即不会再次切换到挂起函数前的线程中。如下 finish 的线程一定是和 withContext 中的线程是同一个
e("c = ${Thread.currentThread().name}")
withContext(Dispatchers.IO) {
    e("with = ${Thread.currentThread().name}")
}
e("finish ${Thread.currentThread().name}")