概念
协程是一种非抢占式或者说协作式的计算机程序并发调度的实现,程序可以主动挂起或者恢复执行。
函数或者一段程序能够被挂起(可以理解成暂停),待会儿再恢复,挂起和恢复是开发者的程序逻辑自己控制的,协程是通过主动挂起出让运行权来实现协作的。
协程和线程区别
- 线程之间是抢占式的调度
- 协程之间是协作式的调度
协程是依赖于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的。所以协程像是一种用户态的线程,非常轻量级,一个线程中可以创建N个协程。
使用
依赖
// 协程核心库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3"
// 协程Android支持库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3"
开启协程
使用 runBlocking 顶层函数
通常适用于单元测试的场景,业务开发中不会用到这种方法,因为它是线程阻塞的。
runBlocking {
// 代码...
}
使用GlobalScope 对象
使用GlobalScope单例对象,可以直接调用launch、async开启协程。在Android开发中同样不推荐这种用法,因为它的生命周期会和app一致,且不能取消(和使用runBlocking的区别在于不会阻塞线程)。
GlobalScope.launch {
// 代码...
}
GlobalScope.async {
// 代码...
}
使用CoroutineScope 对象
通过 CoroutineContext创建一个CoroutineScope对象,需要一个类型为CoroutineContext 的参数(推荐的使用,通过 context 参数去管理和控制协程的生命周期)。
val coroutineScope = CoroutineScope(context)
coroutineScope.launch {
// 代码...
}
作用域
作用域(Coroutine Scope)是协程运行的作用范围。launch、async都是CoroutineScope的扩展函数,CoroutineScope定义了新启动的协程作用范围,同时会继承了他的coroutineContext自动传播其所有的 elements和取消操作。
实操
runBlocking 顶级函数
binding.tvBtn.setOnClickListener {
val result= runBlocking {
Log.e("CoroutineExActivity","---runBlocking--》启动协程")
"使用runBlocking 启动协程"
}
Log.e("CoroutineExActivity","--runBlocking-->${result}")
}
// 执行结果
CoroutineExActivity: ---runBlocking--》启动协程
CoroutineExActivity: --runBlocking-->使用runBlocking 启动协程
runBlocking启动的是一个新的协程并阻塞调用它的线程,直到runBlocking运行结束才继续往下执行。并且在runBlocking协程最后一行增加一个返回值。
launch 函数
binding.tvBtn.setOnClickListener {
val result=GlobalScope.launch {
Log.e("CoroutineExActivity","-----》启动协程")
}
Log.e("CoroutineExActivity","---->${result}")
}
// 执行结果(其中一种结果)
CoroutineExActivity: -----》启动协程
CoroutineExActivity: ---->StandaloneCoroutine{Completed}@b2fd55f
从上面的执行结果来看,launch方法返回StandaloneCoroutine类型的result。查看一下其源码:
// launch 源码
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
}
从源码来看,launch方法返回了Job类型。那执行结果StandaloneCoroutine和源码中定义方法返回类型Job之间有啥关联?查看StandaloneCoroutine源码:
// StandaloneCoroutine 源码
private open class StandaloneCoroutine(
parentContext: CoroutineContext,
active: Boolean
) : AbstractCoroutine<Unit>(parentContext, active) {
...
}
public abstract class AbstractCoroutine<in T>(
/**
* The context of the parent coroutine.
*/
@JvmField
protected val parentContext: CoroutineContext,
active: Boolean = true
) : JobSupport(active), Job, Continuation<T>, CoroutineScope {
...
}
从源码来看,StandaloneCoroutine是Job 的子类。
async 函数
binding.tvBtn.setOnClickListener {
val result=GlobalScope.async {
Log.e("CoroutineExActivity","---async--》启动协程")
}
Log.e("CoroutineExActivity","--async-->${result}")
}
// 执行结果(其中一种结果)
CoroutineExActivity: --async-->DeferredCoroutine{Active}@b2fd55f
CoroutineExActivity: ---async--》启动协程
从上面的执行结果来看,async方法返回DeferredCoroutine类型的result。查看一下其源码:
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T> {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyDeferredCoroutine(newContext, block) else
DeferredCoroutine<T>(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
private open class DeferredCoroutine<T>(
parentContext: CoroutineContext,
active: Boolean
) : AbstractCoroutine<T>(parentContext, active), Deferred<T>, SelectClause1<T> {
...
}
public interface Deferred<out T> : Job {
//返回结果值,或者如果延迟被取消,则抛出相应的异常
public suspend fun await(): T
public val onAwait: SelectClause1<T>
public fun getCompleted(): T
public fun getCompletionExceptionOrNull(): Throwable?
}
从上面的源码来看,DeferredCoroutine是AbstractCoroutine的实现类,同时实现Deferred 接口。这么看来DeferredCoroutine就是一个Deferred,一个携带有返回值Job。 通过
Deferred 的await() 方法来获取返回值T。
// launch 源码
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
...
}
// async 源码
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T> {
...
}
从launch和 async 方法源码来看,CoroutineContext代表上下文,它的作用就是线程切换,它的值有Dispatchers.Main、Dispatchers.IO、、Dispatchers.Default、Dispatchers.Unconfined。
CoroutineStart表示启动模式,其值有:CoroutineStart.DEFAULT、CoroutineStart.LAZY、CoroutineStart.ATOMIC、CoroutineStart.UNDISPATCHED,默认直接允许调度执行。各个值具体含义和用法在稍后文章中讲解。
// 第一种写法
coroutineScope.launch(Dispatchers.Main) { // 在 UI 线程开始
val image = withContext(Dispatchers.IO) { // 切换到 IO 线程,并在执行完成后切回 UI 线程
getImage(imageId) // 将会运行在 IO 线程
}
avatarIv.setImageBitmap(image) // 回到 UI 线程更新 UI
}
// 第二种方法
coroutineScope.launch(Dispatchers.Main) {// 在 UI 线程开始
val image = getImage(imageId) // 调用suspend 函数
avatarIv.setImageBitmap(image) // 回到 UI 线程更新 UI
}
// 挂起函数
suspend fun getImage(imageId: Int) = withContext(Dispatchers.IO) {
...
}
suspend 是 Kotlin 协程最核心的关键字,意思是可挂起(暂停)。挂起函数只能在协程中或者挂起函数中调用。
withContext()函数是一个比较特殊的作用域构建器,withContext()函数是一个挂起函数。调用
withContext()函数之后,会立即执行代码块中的代码,同时将当前协程阻塞住。当代码块中的代码全部执行完之后,会将最后一行的执行结构作为withContext()函数的返回值返回。
协程比线程轻量级原因
编程语言级别实现的协程就是程序内部的逻辑,不会涉及操作系统的资源之间的切换。操作系统的内核线程自然会重一些,且不说每创建一个线程就会开辟的栈带来的内存开销,线程在上下文切换的时候需要CPU把高速缓存清掉并从内存中替换下一个线程的内存数据,并且处理上一个内存的中断点保存就是一个开销很大的事儿。