之前在掘金上对Kotlin协程的解析比较零散,小小地推荐一下《深入理解Kotlin协程》,从源码和实例出发,结合图解,系统地分析 Kotlin 协程启动,挂起,恢复,异常处理,线程切换,并发等流程,只用一顿饭钱!感兴趣的朋友可以了解下,互相交流,不喜勿喷。
概述
在上一篇 Kotlin协程之深入理解协程工作原理 中从源码角度介绍过 Kotlin 协程的工作原理,这一篇文章记录一下 Kotlin 协程的基础使用,熟悉协程开发的同学忽略即可。文中内容如有错误欢迎指出,共同进步!觉得不错的留个赞再走哈~
2019 年 Google I/O 大会上宣布今后将越来越优先采用 Kotlin 进行 Android 开发,Kotlin 是一种富有表现力且简洁的编程语言,不仅可以减少常见代码错误,还可以轻松集成到现有应用中,用过 Kotlin 的同学估计都会觉得 “真香”。关于 Kotlin 的基础用法可参考: Kotlin 官方文档 和 Kotlin 基础笔记。
通常来说,协程(Coroutines)是轻量级的线程,它不需要从用户态切换到内核态,Coroutine是编译器级的,Process 和 Thread 是操作系统级的,协程没有直接和操作系统关联,但它也是跑在线程中的,可以是单线程,也可以是多线程。协程设计的初衷是为了解决并发问题,让协作式多任务实现起来更加方便,它可以有效地消除回调地狱。
在Kotlin中,协程是一套线程 API, 就像 Java 的 Executors 和 Android 的 Handler 等,是一套比较方便的线程框架,它能够在同一个代码块里进行多次的线程切换,可以用看起来同步的方式写出异步的代码,即非阻塞式挂起。
协程的优势:避免回调地狱(也可以通过 RxJava 或者 CompletableFuture 实现):
// 回调
api.getAvatar(id) { avatar ->
api.getName(id) { name ->
show(getLabel(avatar, name))
}
}
// 协程
coroutineScope.launch(Dispatchers.Main) {
val avatar = async { api.getAvatar(id) }
val name = async { api.getName(id) }
val label = getLabelSuspend(avatar.await(), name.await())
show(label)
}
使用时需要先添加依赖:
def kotlin_coroutines = `1.3.9`
// 依赖协程核心库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines"
// 依赖当前平台所对应的平台库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines"
启动协程可以使用下面几种方式:
// 线程阻塞,适用于单元测试的场景
runBlocking { getName(id) }
// 不会阻塞线程,但在 Android 中不推荐,因为它的生命周期会和 app 一致
GlobalScope.launch { getName(id) }
// 推荐使用,通过 CoroutineContext 参数去管理和控制协程的生命周期
// 例如:context = Dispatchers.Default + EmptyCoroutineContext
val coroutineScope = CoroutineScope(context)
coroutineScope.launch { getName(id) }
// async启动的Job是Deferred类型,它可以有返回结果,通过await方法获取
// public suspend fun await(): T
val id = coroutineScope.async { getName(id) }
id.await()
启动协程需要三样东西,分别是上下文(CoroutineContext)、启动模式(CoroutineStart)、协程体。
基本概念
suspend
suspend 是挂起的意思,它挂起的对象是协程。
- 当线程执行到协程的 suspend 函数的时候,暂时不继续执行协程代码了,它会跳出协程的代码块,然后这个线程该干什么就去干什么。
- 当协程执行到 suspend 函数的时候,这个协程会被「suspend」,也就是从当前线程被挂起。换句话说,就是这个协程从正在执行它的线程上脱离。线程的代码在到达 suspend 函数的时候被掐断,接下来协程会从这个 suspend 函数开始继续往下执行,不过是在这个 suspend 函数指定的线程里执行。
紧接着在 suspend 函数执行完成之后,协程会自动帮我们把线程再切回来,然后接着执行协程后面的代码(resume),resume 功能是协程特有的,所以 suspend 函数必须在 协程 或者 另一个suspend函数 里被调用。
suspend 的非阻塞式指的是它能用看起来阻塞的代码写出非阻塞的操作,单协程可以是非阻塞式的,因为它可以用 suspend 函数来切换线程,本质上还是多线程,只不过写法上连续两行代码看起来是阻塞式的。
CoroutineScope
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
GlobeScope
GlobeScope 启动的协程是一个单独的作用域,不会继承上层协程的作用域,其内部的子协程遵守默认的作用域规则。
coroutineScope
coroutineScope 启动的协程 cancel 时会 cancel 所有子协程,也会 cancel 父协程,子协程未捕获的异常也会向上传递给父协程。
supervisorScope
supervisorScope 启动的协程 cancel 和传递异常时,只会由父协程向子协程单向传播。MainScope 是 supervisorScope 作用域。
CoroutineContext
Job, CoroutineDispatcher, ContinuationInterceptor 等都是 CoroutineContext 的子类,即它们都是协程上下文。CoroutineContext 中有一个重载了(+)操作符的plus方法,可以将 Job 和 CoroutineDispatcher 等元素集合起来,代表一个协程的场景。
public interface CoroutineContext {
// 重载 [] 操作符
public operator fun <E : Element> get(key: Key<E>): E?
public fun <R> fold(initial: R, operation: (R, Element) -> R): R
// 重载 + 操作符
public operator fun plus(context: CoroutineContext): CoroutineContext = /* ... */
public fun minusKey(key: Key<*>): CoroutineContext
public interface Key<E : Element>
public interface Element : CoroutineContext {
// ...
}
}
public interface ContinuationInterceptor : CoroutineContext.Element {
// ...
}
public abstract class CoroutineDispatcher :
AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
// ...
}
CoroutineStart
几种启动模式如下:
public enum class CoroutineStart {
DEFAULT,
LAZY,
ATOMIC,
UNDISPATCHED; // 立即在当前线程执行协程体,直到第一个suspend调用
}
DEFAULT
DEFAULT是饿汉式启动,launch调用后,会立即进入待调度状态,一旦调度器OK就可以开始执行。
LAZY
LAZY是懒汉式启动,launch后并不会有任何调度行为,协程体也不会进入执行状态,直到需要它执行(调用job.start/join等)的时候才会执行。
ATOMIC
@ExperimentalCoroutinesApi
. This is similar to [DEFAULT], but the coroutine cannot be cancelled before it starts executing.
UNDISPATCHED
@ExperimentalCoroutinesApi
. This is similar to [ATOMIC] in the sense that coroutine starts executing even if it was already cancelled, but the difference is that it starts executing in the same thread.
Dispatchers
协程调度器是用来指定协程体在哪个线程中执行,Kotlin提供了几个调度器:
Default
默认选项,指定协程体在线程池中执行:
GlobalScope.launch(Dispatchers.Default) {
println("1: ${Thread.currentThread().name}")
launch(Dispatchers.Default) {
println("2: ${Thread.currentThread().name}")
}
println("3: ${Thread.currentThread().name}")
}
-->output
1: DefaultDispatcher-worker-1
3: DefaultDispatcher-worker-1
2: DefaultDispatcher-worker-2
Main
指定协程体在主线程中执行。
Unconfined
协程体运行在父协程所在的线程:
GlobalScope.launch(Dispatchers.Default) {
println("1: ${Thread.currentThread().name}")
launch(Dispatchers.Unconfined) {
println("2: ${Thread.currentThread().name}")
}
println("3: ${Thread.currentThread().name}")
}
-->output
1: DefaultDispatcher-worker-1
2: DefaultDispatcher-worker-1
3: DefaultDispatcher-worker-1
IO
基于 Default 调度器背后的线程池(designed for offloading blocking IO tasks):
GlobalScope.launch(Dispatchers.Default) {
println("1: ${Thread.currentThread().name}")
launch(Dispatchers.IO) {
println("2: ${Thread.currentThread().name}")
}
println("3: ${Thread.currentThread().name}")
}
-->output
1: DefaultDispatcher-worker-1
3: DefaultDispatcher-worker-1
2: DefaultDispatcher-worker-1
Job&Deferred
Job 可以取消并且有简单生命周期,它有三种状态:
State | isActive | isCompleted | isCancelled |
---|---|---|---|
New (optional initial state) | false | false | false |
Active (default initial state) | true | false | false |
Completing (optional transient state) | true | false | false |
Cancelling (optional transient state) | false | false | true |
Cancelled (final state) | false | true | true |
Completed (final state) | false | true | false |
Job 完成时是没有返回值的,如果需要返回值的话,应该使用 Deferred,它是 Job 的子类: public interface Deferred<out T> : Job
。
launch 方法返回一个Job类型:
public interface Job : CoroutineContext.Element {
public companion object Key : CoroutineContext.Key<Job> {
// ...
}
public val isActive: Boolean
public val isCompleted: Boolean
public val isCancelled: Boolean
public fun start(): Boolean
public fun cancel(): Unit = cancel(null)
public suspend fun join()
// ...
}
async 方法返回一个 Deferred 类型:
public interface Deferred<out T> : Job {
// 该方法可以得到返回值
public suspend fun await(): T
// ...
}
示例:
fun main() = runBlocking {
val job = async {
delay(500)
"Hello"
}
println("${job.await()}, World")
}
Android-Kotlin协程使用
MainScope
Android 中一般不建议使用 GlobalScope, 因为它会创建一个顶层协程,需要保持所有对 GlobalScope 启动的协程的引用,然后在 Activity destory 等场景的时候 cancel 掉这些的协程,否则就会造成内存泄露等问题。可以使用 MainScope:
class CoroutineActivity : AppCompatActivity() {
private val mainScope = MainScope()
fun request1() {
mainScope.launch {
// ...
}
}
// request2, 3, ...
override fun onDestroy() {
super.onDestroy()
mainScope.cancel()
}
}
MainScope 的定义:
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
Lifecycle协程
关于 Lifecycle 可以参考 Android-Jetpack组件之Lifecycle。
添加依赖:
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
源码如下:
val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
get() = lifecycle.coroutineScope
val Lifecycle.coroutineScope: LifecycleCoroutineScope
get() {
while (true) {
val existing = mInternalScopeRef.get() as LifecycleCoroutineScopeImpl?
if (existing != null) {
return existing
}
val newScope = LifecycleCoroutineScopeImpl(
this,
SupervisorJob() + Dispatchers.Main.immediate
)
if (mInternalScopeRef.compareAndSet(null, newScope)) {
newScope.register()
return newScope
}
}
}
abstract class LifecycleCoroutineScope internal constructor() : CoroutineScope {
internal abstract val lifecycle: Lifecycle
// 当 activity created 的时候执行协程体
fun launchWhenCreated(block: suspend CoroutineScope.() -> Unit): Job = launch {
lifecycle.whenCreated(block)
}
// // 当 activity started 的时候执行协程体
fun launchWhenStarted(block: suspend CoroutineScope.() -> Unit): Job = launch {
lifecycle.whenStarted(block)
}
// // 当 activity resumed 的时候执行协程体
fun launchWhenResumed(block: suspend CoroutineScope.() -> Unit): Job = launch {
lifecycle.whenResumed(block)
}
}
使用:
// AppCompatActivity 实现了 LifecycleOwner 接口
class MainActivity : AppCompatActivity() {
fun test() {
lifecycleScope.launchWhenCreated {
// ...
}
}
}
LiveData协程
关于 LiveData 可以参考 Android-Jetpack组件之LiveData-ViewModel。
添加依赖:
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
源码如下:
fun <T> liveData(
context: CoroutineContext = EmptyCoroutineContext,
timeoutInMs: Long = DEFAULT_TIMEOUT,
@BuilderInference block: suspend LiveDataScope<T>.() -> Unit
): LiveData<T> = CoroutineLiveData(context, timeoutInMs, block)
internal class CoroutineLiveData<T>(
context: CoroutineContext = EmptyCoroutineContext,
timeoutInMs: Long = DEFAULT_TIMEOUT,
block: Block<T>
) : MediatorLiveData<T>() {
private var blockRunner: BlockRunner<T>?
private var emittedSource: EmittedSource? = null
init {
val supervisorJob = SupervisorJob(context[Job])
val scope = CoroutineScope(Dispatchers.Main.immediate + context + supervisorJob)
blockRunner = BlockRunner(
liveData = this,
block = block,
timeoutInMs = timeoutInMs,
scope = scope
) {
blockRunner = null
}
}
internal suspend fun emitSource(source: LiveData<T>): DisposableHandle {
clearSource()
val newSource = addDisposableSource(source)
emittedSource = newSource
return newSource
}
internal suspend fun clearSource() {
emittedSource?.disposeNow()
emittedSource = null
}
// 启动协程
override fun onActive() {
super.onActive()
blockRunner?.maybeRun()
}
// 取消协程
override fun onInactive() {
super.onInactive()
blockRunner?.cancel()
}
}
使用:
// AppCompatActivity 实现了 LifecycleOwner 接口
class MainActivity : AppCompatActivity() {
fun test() {
liveData {
try {
// ...
emit("success")
} catch(e: Exception) {
emit("error")
}
}.observe(this, Observer {
Log.d("LLL", it)
})
}
}
ViewModel协程
关于 ViewModel 可以参考 Android-Jetpack组件之LiveData-ViewModel。
添加依赖:
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
源码如下:
val ViewModel.viewModelScope: CoroutineScope
get() {
val scope: CoroutineScope? = this.getTag(JOB_KEY)
if (scope != null) {
return scope
}
return setTagIfAbsent(JOB_KEY,
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate))
}
internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
override val coroutineContext: CoroutineContext = context
override fun close() {
coroutineContext.cancel()
}
}
协程的并发
启动一百个协程,它们都做一千次相同的操作,同时会测量它们的完成时间以便进一步的比较:
suspend fun massiveRun(action: suspend () -> Unit) {
val n = 100 // 启动的协程数量
val k = 1000 // 每个协程重复执行同一动作的次数
val time = measureTimeMillis {
coroutineScope { // 协程的作用域
repeat(n) {
launch {
repeat(k) { action() }
}
}
}
}
println("Completed ${n * k} actions in $time ms")
}
var counter = 0
fun main() = runBlocking {
withContext(Dispatchers.Default) {
massiveRun {
counter++
}
}
println("Counter = $counter")
}
// output
Completed 100000 actions in 55 ms
Counter = 92805
因为一百个协程在多个线程中同时递增计数器但没有做并发处理,所以不太可能输出 100000 。使用 volatile 并不能解决这个问题,因为 volatile 不能保证原子性,只有内存可见性。可以用以下等方式解决:
使用原子类 AtomicInteger 进行递增
以细粒度限制线程:对特定共享状态的所有访问权都限制在单个线程中
val counterContext = newSingleThreadContext("CounterContext")
var counter = 0
// 运行比较慢
fun main() = runBlocking {
withContext(Dispatchers.Default) {
massiveRun {
// 将每次自增限制在单线程上下文中
withContext(counterContext) {
counter++
}
}
}
println("Counter = $counter")
}
// output
Completed 100000 actions in 1652 ms
Counter = 100000
以粗粒度限制线程:线程限制在大段代码中执行
val counterContext = newSingleThreadContext("CounterContext")
var counter = 0
fun main() = runBlocking {
// 将一切都限制在单线程上下文中
withContext(counterContext) {
massiveRun {
counter++
}
}
println("Counter = $counter")
}
// output
Completed 100000 actions in 40 ms
Counter = 100000
互斥:除了Java已有的一些互斥方式如 Lock 等之外,Kotlin 中提供了 Mutex 类,它具有 lock 和 unlock 方法,lock 是一个挂起函数,它不会阻塞线程。可以使用 withLock 扩展函数替代常用的 mutex.lock(); try { …… } finally { mutex.unlock() }
val mutex = Mutex()
var counter = 0
// 此示例中锁是细粒度的,因此会付出一些代价。
fun main() = runBlocking {
withContext(Dispatchers.Default) {
massiveRun {
// 用锁保护每次自增
mutex.withLock {
counter++
}
}
}
println("Counter = $counter")
}
// output
Completed 100000 actions in 640 ms
Counter = 100000
总结
这篇文章主要记录一下 Kotlin 协程的相关基础使用,要是对 Kotlin 协程工作原理感兴趣的话可以看看这篇 Kotlin协程之深入理解协程工作原理,主要讲到 Kotlin 中的协程存在着三层包装,每层的工作如下图:
参考: