1.1-协程基础与关键知识:切线程 launch

为什么要切线程?

在工程项目中,我们切线程是不希望要切线程的代码挡住我当前的线程,所以需要开一个并行的线程执行这段代码。在 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,但在使用后需要手动关闭

  • 18
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值