Kotlin协程之基础使用

5,817 阅读8分钟

之前在掘金上对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 可以取消并且有简单生命周期,它有三种状态:

StateisActiveisCompletedisCancelled
New (optional initial state)falsefalsefalse
Active (default initial state)truefalsefalse
Completing (optional transient state)truefalsefalse
Cancelling (optional transient state)falsefalsetrue
Cancelled (final state)falsetruetrue
Completed (final state)falsetruefalse

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 中的协程存在着三层包装,每层的工作如下图:

参考: