文章目录
为什么要切线程?
在工程项目中,我们切线程是不希望要切线程的代码挡住我当前的线程,所以需要开一个并行的线程执行这段代码。在 Java 中开启一个线程很简单,可以使用 Thread:
Thread {
// 执行代码
}.start()
// kotlin 的简便写法
thread {
// 执行代码
}
不过在真正的开发过程我们一般不会直接创建 Thread,而是用线程池,它可以复用线程让软件性能更好:
val executor = Executors.newCachedThreadPool()
executor.execute {
// 执行代码
}
通常我们会把这种并行线程叫做子线程或后台线程。
而不希望被挡住的线程在 Android 中主要代指的主线程,因为界面的更新指定了要在主线程执行;切回主线程执行的代码可以用 handler.post 或 view.post:
val handler = Handler(Looper.getMainLooper())
handler.post {
// 主线程执行代码
}
view.post {
// 主线程执行代码
}
在有 kotlin 后,kotlin 提供了另外一种切线程的方式:协程。
启动一个协程
在 Android 项目中使用协程需要引入依赖,在项目的 build.gradle 添加依赖:
build.gradle
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1"
}
引入依赖后就可以在项目中使用协程:
val scope = CoroutineScope(EmptyCoroutineContext)
scope.launch {
// 执行代码
}
在协程中是 使用的 CoroutineScope 启动一个协程,写法上有点类似 Java 的线程池 Executors,CoroutineScope 实际上也有线程池的功能,但线程池只是它功能重要的一部分。
EmptyCoroutineContext 的类型是 CoroutineContext,CoroutineContext 会提供启动协程会用到的各种上下文信息,比如线程池,EmptyCoroutineContext 表示是一个空的上下文信息:
CoroutineScope.kt
@Suppress("FunctionName")
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
ContextScope(if (context[Job] != null) context else context + Job())
开启一个协程用 scope.launch,但 JVM 的 kotlin 协程实际上也是切线程,至于具体切换到哪个线程由 CoroutineScope 决定。
指定协程调度:ContinuationInterceptor 和 CoroutineDispatcher
ContinuationInterceptor 和 CoroutineDispatcher 的关系
scope.launch 启动一个协程,协程会使用默认的线程池来执行任务,确切的说不是线程池而是线程,因为有时候我们还需要把任务扔到 UI 线程。
在协程里这类 [管理任务执行的线程] 的工具是 ContinuationInterceptor,它的作用是 [在代码往下执行之前先拦截住,做点别的工作,再继续执行]。
提到 ContinuationInterceptor 就需要说到 CoroutineDispatcher,它是 ContinuationInterceptor 的子类:
public abstract class CoroutineDispatcher :
AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {}
kotlin 提供了 4 个直接可用的 ContinuationInterceptor,更准确的说是 4 个 CoroutineDispatcher,它们是做 [任务调度] 也就是切线程的:
Dispatchers.kt
public actual object Dispatchers {
@JvmStatic
public actual val Default: CoroutineDispatcher = DefaultScheduler
@JvmStatic
public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
@JvmStatic
public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined
@JvmStatic
public val IO: CoroutineDispatcher = DefaultIoScheduler
}
Dispatchers.Default 和 Dispatchers.IO
如果在启动一个协程时没有指定任何的 CoroutineDispatcher,scope.launch 将会使用 Dispatchers.Default 来调度任务,它提供了一个全局的线程池管理任务,启动协程会在它提供的线程池里去运行。
在项目中用的比较多的 CoroutineDispatcher 是 Dispatcher.Default 和 Dispatcher.IO,Dispatchers.Default 主要用于计算密集型任务,Dispatchers.IO 主要用于 IO 密集型任务,关于两者的区别可以具体查看 线程池,对计算密集型和 IO 密集型有详细的说明,这里不再赘述。
如果我们要将协程指定到对应的任务调度,可以在创建 CoroutineScope 时指定:
val scope = CoroutineScope(Dispatchers.Default)
scope.launch {
// 在 Dispatchers.Default 指定的线程池执行
}
val scope = CoroutineScope(Dispatchers.IO)
scope.launch {
// 在 Dispatchers.IO 指定的线程池执行
}
除了上面的方式,还可以在 scope.launch 参数里面配置:
// 复用 CoroutineDispatcher 任务调度,在 CoroutineScope 传参
val scope = CoroutineScope(Dispatchers.IO)
scope.launch {}
scope.launch {}
// scope.launch 传参会覆盖 CoroutineScope 传参
val scope = CoroutineScope(EmptyCoroutineContext)
scope.launch(Dispatchers.IO) {
// 在 Dispatchers.IO 指定的线程池执行
}
两种方式的区别是:
-
如果 scope 要多次 launch 在同一个线程池执行,建议在 CoroutineScope 传参
-
在 scope.launch 传参会覆盖 CoroutineScope 传参,即 launch 传参会高于 CoroutineScope
Dispatchers.Main
第三个 ContinuationInterceptor 是 Dispatchers.Main,它提供的不是线程池,而是会把任务扔到主线程去执行:
val scope = CoroutineScope(Dispatchers.Main)
scope.launch {
// 任务回到主线程执行
}
Dispatchers.Main 实际上最终也是用 Handler 将我们代码切回到主线程运行。
自定义创建线程池:newFixedThreadPoolContext 和 newSingleThreadContext
在项目中比较多的就用这三个 ContinuationInterceptor,如果有特殊情况想自己创建线程池,协程也提供了创建方式,newFixedThreadPoolContext 和 newSingleThreadContext:
// 传参指定线程池核心线程数量
val context = newFixedThreadPoolContext(4, "MyThread")
// 单线程的线程池
val context1 = newSingleThreadContext("MyThread")
val scope = CoroutineScope(context)
...
// 使用完后要关闭
context.close()
但用上面的方式在 IDE 会有警告提示,因为它被注解为 @DelicateCoroutinesApi,提示直接使用它是容易出错的,使用后要及时关闭:
@DelicateCoroutinesApi
public actual fun newFixedThreadPoolContext(nThreads: Int, name: String): ExecutorCoroutineDispatcher {
require(nThreads >= 1) { "Expected at least one thread, but $nThreads specified" }
val threadNo = AtomicInteger()
val executor = Executors.newScheduledThreadPool(nThreads) { runnable ->
val t = Thread(runnable, if (nThreads == 1) name else name + "-" + threadNo.incrementAndGet())
t.isDaemon = true
t
}
return executor.asCoroutineDispatcher()
}
相比提到的三个常用的 CoroutineDispatcher,因为它们是全局的,所以就没有需要关闭的场景,不需要自己手动关闭。
Dispatchers.unconfined
最后一个 ContinuationInterceptor 是 Dispatchers.unconfined,像字面意思所说就是 [不限制],它是一个完全不进行线程管理的 ContinuationInterceptor。
挂起函数在该任务调度是不适用的,它不仅在启动协程时不会切线程,而且在挂起函数执行完之后也不会把线程切回去,而是继续在挂起函数所在的那个线程继续执行下面的代码。这种逻辑会让结果非常难以预期,所以实际开发根本不会用到它。
总结
-
切线程是不希望要切线程的代码挡住我当前的线程,在 Java 分别可以用 Thread 或线程池 Executor 的方式创建一个并行线程,在 kotlin 还可以创建 CoroutineScope 使用协程 scope.launch 方式实现
-
在协程里这类 [管理任务执行的线程] 的工具是 ContinuationInterceptor,它的作用是 [在代码往下执行之前先拦截住,做点别的工作,再继续执行]
-
CoroutineDispatcher 是 ContinuationInterceptor 的子类,协程库提供了 4 个 CoroutineDispatcher 做 [任务调度] 也就是切线程的,其中最常用的是 Dispatchers.Default 和 Dispatchers.IO,分别在计算密集型任务和 IO 密集型任务执行代码;Dispatchers.Main 是将代码扔回主线程执行
-
将 CoroutineDispatchers 分别在 CoroutineScope 和 scope.launch 传参,考虑复用场景建议在 CoroutineScope 传参;launch 传参优先级会高于 CoroutineScope
-
特殊情况需要自定义线程池可以通过 newFixedThreadPoolContext 和 newSingleThreadContext,但在使用后需要手动关闭