Kotlin 协程概览

Kotin 协程的概念

Kotlin中协程是指一段可以被 挂起 的代码执行过程,类似于线程被挂起从而让出CPU资源,协程被挂起是为了让出线程资源,因此协程也可以被理解成是轻量级线程。

在此前《Java线程模型》 一文中,我们知道主流操作系统目前使用的都是内核线程模型,而协程即相当于实现了混合线程模型中的用户态线程。

使用协程的简单示例

下面给出一个简单的使用Kotlin协程的示例代码:

GlobalScope.launch {
    delay(1000L)
    println("World!")
}
println("Hello")

上述代码表示通过 launch 函数发起了一个协程,在这个协程里调用 delay 函数延迟1000ms之后再输出 "World"。delay 函数类似于线程中使用的 sleep 函数,sleep 函数是使得线程休眠一段时间从而进入被挂起的状态,而 delay 函数是使得协程休眠一段时间从而进入被挂起的状态。

上述代码的输出结果是:

Hello
World

挂起协程

通过上述示例可以更好地理解协程的挂起,协程的挂起是将当前协程从执行的线程里挂起,从而让出线程资源去执行其他的代码。

由于调用 delay 函数使得协程被挂起,就会先执行协程之后的代码,即先输出了"Hello",然后在大约1000ms后协程又获得了线程资源继续执行,输出了"World"。

协程的作用域和上下文

上述示例代码中我们通过 launch 函数发起了一个协程,launch 函数是一个扩展函数,函数的 receiver 类型是 CoroutineScope,返回类型是 Job
CoroutineScope 表示一个协程的作用域,协程的作用域封装了协程执行的上下文、协程的生命周期管理等功能。

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

CoroutineScope 类是一个抽象的接口类型,其只包含一个CoroutineContext类型的成员 coroutineContext

CoroutineContext 类是一个抽象的接口类型,表示一个协程执行的上下文信息,其接口类似于一个Map类型,保存了一系列与协程相关的key-value键值对。

前述示例中的 GlobalScope 是一个单例实现,表示一个全局的CoroutineScope,其生命周期与整个应用的生命周期相同。GlobalScope 的成员coroutineContextEmptyCoroutineContext单例,表示一个空的上下文。

Job 表示协程执行的任务,该任务是可以被取消的。Job 类是一个抽象接口,继承了 CoroutineContext.Element 接口, Eelement是CoroutineContext的一个子类,表示协程上下文中存储的key-value,其包含一个名为key的成员变量,在协程上下文中即可以通过 get(key: Key) 函数来获取某个Element。

public interface Job : CoroutineContext.Element {
}

public interface Element : CoroutineContext {
    public val key: Key<*>
}

//获取GlobbalScope中当前正在执行的Job,并取消执行
GlobalScope.coroutineContext[Job.Key]?.cancel()

再深入去看launch函数,launch函数实际上接收3个参数,最后一个参数即lambada表达式,表示要放在协程中执行的一段代码。第一个参数context是CoroutineContext类型,表示需要额外添加的一些上下文信息,默认是EmptyCoroutineContext。

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy) 
	LazyStandaloneCoroutine(newContext, block)
    else
	StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {
    val combined = coroutineContext + context
    return if (combined ! == Dispatchers.Default && combined[ContinuationInterceptor] == null)
        combined + Dispatchers.Default else combined
}

private open class StandaloneCoroutine(
    parentContext: CoroutineContext,
    active: Boolean
) : AbstractCoroutine<Unit>(parentContext, initParentJob = true, active = active) {
    override fun handleJobException(exception: Throwable): Boolean {
        handleCoroutineException(context, exception)
        return true
    }
}

可以看到launch函数的实现中会调用newCoroutineContext函数产生一个新的CoroutineContext实例作为其后创建的协程的 parentContext

newCoroutineContext函数中会把launch函数中传进来的context与当前CoroutineScope的CoroutineContext进行合并产生CombinedContext,相同Key的元素会被launch函数中传进来的context中的元素所覆盖。CombinedContext是CoroutineContext的子类。
newCorotineContext函数中还会检查是否有指定CoroutineDispatcher,如果没有则会默认使用Dispatchers.Default,CoroutineDispatcher表示协程执行所在的线程。

launch函数创建的协程实现类是StandalongCoroutine,其继承自AbstractCoroutine,AbstctCoroutine表示一个协程的实现,继承自 JobSupport 类并且实现了CoroutineScope接口。

public abstract class AbstractCoroutine<in T>(
    parentContext: CoroutineContext,
    initParentJob: Boolean,
    active: Boolean
) : JobSupport(active), Job, Continuation<T>, CoroutineScope {

    init {
        if (initParentJob) initParentJob(parentContext[Job])
    }

    public final override val context: CoroutineContext = parentContext + this

}

新创建的协程的CoroutineContext继承了 parentContext 中的所有元素,包括CoroutineExceptionHandler、CoroutineDispatcher等,并且覆盖了Job设置为自己本身,而parentContext中的Job作为自身的Parent Job。

协程执行所在的线程

CoroutineDispatcher 是一个抽象类,继承自 AbstractCoroutineContextElement 类,AbstractCoroutineContextElement实现了CoroutineContext.Element接口,可以作为协程上下文信息中的一个元素。

public abstract class CoroutineDispatcher :
    AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
}

CorotineDispatcher 表示如何将协程分发到特定的线程去执行,Kotlin提供了几个常用的实现,例如:

  • Dispathers.Default
    适合用于执行一些耗费CPU资源的计算类型的任务。底层使用全局共享的线程池,最多可以并发执行与cpu数目相同的协程数目。

  • Dispatchers.Main
    适用于执行不耗时可以在主线程执行的任务。 所有的协程任务都会被分发到主线程去执行。

  • Dispatchers.IO
    适合用于执行一些IO类型的任务。底层使用全局共享的线程池,最多可以并发执行64(由系统配置)个协程任务。

  • Dispatchers.Unconfined
    适合于一些场景下协程中的部分代码需要立即执行。协程首次执行任务所在的线程是发起协程调用时所在的线程,首次挂起之后,协程再次执行所在的线程则是不确定的,取决于挂起函数将协程切到哪个线程去执行。

  • newSingleThreadContext
    创建一条独立的线程来执行协程中的代码。协程中的代码始终会在独立的一条线程中执行。

发起协程的其他方法

除了上述通过 launch 函数发起一个协程,我们还可以使用 async 函数发起一个协程:

val deferred = GlobalScope.async {
    delay(1000L)
    println("World!")
}
deferred.await()
println("Hello")

与 launch 函数一样,async也是一个扩展函数,函数的receiver类型是CoroutineScope类型,不同的是 async 函数返回的类型是 Deferred 类型。

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T> {
	...
}

Deferred 类继承自 Job 类,表示一个具有返回值的任务,可以通过 getCompleted() 函数获取返回值,或者通过 await() 函数等待返回值成功返回。Job 相当于Java线程框架中的Runnable类,而Deferred则相当于Java线程框架中的FutureTask类。

posted @ 2023-08-30 12:49  jqc  阅读(65)  评论(0编辑  收藏  举报