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 的成员coroutineContext是EmptyCoroutineContext单例,表示一个空的上下文。
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类。