kotlin 协程基础

一、简介

协程是一种并发设计模式,您可以在 Android 平台上使用它来简化异步执行的代码。协程是一段可以挂起的代码,协程可以看作是轻量级的线程。
协程与线程的关系:
(1)一个线程中可以创建任意个协程;
(2)协程的执行、挂起、恢复等依赖于线程,但是协程挂起时不需要阻塞线程;
(3)协程不一定要指定某个线程,即协程可以在一个线程中挂起,然后在另外一个线程中恢复;

1.1 协程挂起恢复原理

每一个suspend修饰的方法或者lambda表达式都会在编译后,为其额外添加Continuation类型的参数,同时返回了Any?类型。这个Continuation对象通过类似回调的方式管理协程的挂起和恢复。Continuation定义如下:

/**
 * Interface representing a continuation after a suspension point that returns a value of type `T`.
 */
@SinceKotlin("1.3")
public interface Continuation<in T> {
    /**
     * The context of the coroutine that corresponds to this continuation.
     */
    public val context: CoroutineContext

    /**
     * Resumes the execution of the corresponding coroutine passing a successful or failed [result] as the
     * return value of the last suspension point.
     */
    public fun resumeWith(result: Result<T>)
}

当suspend函数被协程挂起时,它会返回一个特殊的标识COROUTINE_SUSPENDED,而它本质就是一个Any;当协程不挂起进行执行时,它将返回执行的结果或者引发的异常。这样为了让这两种情况的返回都支持,所以使用了Kotlin独有的Any?类型。

1.2 协程轻量级原理

(1)占用空间小
每个线程都有自己的堆栈,通常为1MB。JVM中每个线程允许的最小堆栈空间是64k,而Kotlin中的一个简单协程只占用几十字节的堆内存。但是协程依赖的Dispatchers有线程数量的限制。
(2)挂起恢复代价小
通过一个类似回调的对象Continuation将协程管理为挂起和恢复,该对象Continuation作为最后一个参数添加到编译时标记为suspend关键字的函数中,该函数与其他对象一样位于堆中,负责恢复协程,因此,RAM中不需要数千MB的空间来保持所有线程的活动。一个典型的60-70线程是使用CommonPool在max处创建的,并且被重用(如果创建了新的协程,它将等待另一个线程完成)。

二、基础用法

2.1 添加依赖

首先,在project的build.gradle中添加对kotlin插件的依赖

buildscript {
    ext.kotlin_version = '1.5.0'
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

然后,在module中依赖kotlin标准库和kotlin coroutine:

apply plugin: 'kotlin-android'

dependencies {
	// kotlin标准库
	implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
	// kotlin coroutine
   implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0-RC'
}

2.2 简单示例

// launch是协程的builder,它启动了一个协程,这个协程可独立运行
GlobalScope.launch {
	// 挂起当前协程;delay是一个挂起函数:public suspend fun delay(timeMillis: Long);协程的挂起不会阻塞当前线程;
	delay(1000)
	Log.d("CoroutineTag", "Kotlin Coroutines World!") 
}
Log.d("CoroutineTag", "hello") 

运行结果如下:

>hello
>Kotlin Coroutines World!

2.3 GlobalScope.launch源码分析

在2.2的代码中,GlobalScope是CoroutineScope的一个实例;launch是CoroutineScope的扩展函数,用来启动协程,扩展函数定义如下:

/**
 * 启动一个新的协程,并把该协程以Job形式返回;
 * 可通过Job的start()、cancle()、join()等方法控制该协程的执行;
 *
 * 接收三个参数
 * @param context 协程上下文
 * @param start 协程启动方式,默认值是CoroutineStart.DEFAULT 
 * @param block 需要被调用的挂起函数
 * @return job 以Job形式返回新创建的协程
 **/
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
}

下面分别分析CoroutineScope、CoroutineScope.launch()、CoroutineContext、CoroutineStart来介绍协程机制。

三、CoroutineScope

Coroutine Scope即协程作用域,是协程运行的作用范围;每个异步操作都在特定范围内运行。其定义如下:

/**
 * CoroutineScope定义了协程作用范围;
 * 通过CoroutineScope.launch或CoroutineScope.aync创建一个新的CoroutineScope并启动一个新的协程时,新的CoroutineScope会同时继承外部CoroutineScope的coroutineContext;
 */
public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

3.1 创建CoroutineScope

使用合适的CoroutineScope,并在合适的时机销毁,避免内存泄漏。例如在Activity销毁时取消CoroutineScope内的协程,避免内存泄漏;
创建CoroutineScope的方式:
(1)CoroutineScope();通用的CoroutineScope();
(2)MainScope();作用域为UI生命周期,默认调度器为Dispatchers.Main;如下所示:

class MyAndroidActivity {
    private val scope = MainScope()

    override fun onDestroy() {
        super.onDestroy()
        scope.cancel()
    }
}

以下介绍常见的CoroutineScope及会创建CoroutineScope的场景:

3.2 GlobalScope

GlobalScope作用域中的协程在App启动后可一直执行至该协程执行结束或取消,常用来启动一些需要在application生命周期内运行且不能提前取消的顶级协程。
对于一些Activity或Fragment销毁后就不在需要执行的协程,不建议使用GlobalScope来启动。

3.3 CoroutineScope.launch

GlobalScope.launch {
	Log.d("CoroutineTag", "GlobalScope") 
	launch {
		// 协程内的this即CoroutineScope
		Log.d("CoroutineTag", "coroutineScope") 
	}
}

通过CoroutineScope.launch的无参启动方式,会创建一个CoroutineScope,这个CoroutineScope继承了外面CoroutineScope的CoroutineContext。

3.4 CoroutineScope.aync

GlobalScope.launch {
	Log.d("CoroutineTag", "GlobalScope") 
	async {
		// 协程内的this即CoroutineScope
		Log.d("CoroutineTag", "coroutineScope") 
	}
}

通过CoroutineScope.async的无参启动方式,会创建一个CoroutineScope,这个CoroutineScope继承了外面CoroutineScope的CoroutineContext。
CoroutineScope.async与CoroutineScope.launch的区别在于,async方式返回Deferred,可以获取协程的执行结果。

3.5 coroutineScope

在其他协程或挂起函数内,可通过coroutineScope来创建一个CoroutineScope,并且会立即执行coroutineScope{}内新建的协程,如下所示:

GlobalScope.launch {
	Log.d("CoroutineTag", "GlobalScope") 
	coroutineScope {
		// 协程内的this即CoroutineScope
		Log.d("CoroutineTag", "coroutineScope") 
	}
}

coroutineScope是一个挂起挂起函数,所以只能在协程或挂起函数内调用:

public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return suspendCoroutineUninterceptedOrReturn { uCont ->
        val coroutine = ScopeCoroutine(uCont.context, uCont)
        coroutine.startUndispatchedOrReturn(coroutine, block)
    }
}

3.6 runBlocking

在线程、协程、挂起函数内,可通过runBlocking创建一个CoroutineScope,并且会立即执行runBlocking{}内新建的协程,如下所示:

GlobalScope.launch {
	Log.d("CoroutineTag", "GlobalScope") 
	runBlocking {
		// 协程内的this即CoroutineScope
		Log.d("CoroutineTag", "coroutineScope") 
	}
}

与coroutineScope不同的是,runBlocking会阻塞当前线程;runBlocking是一个普通方法,所以runBlocking可以在线程中调用:

/**
 * @param block 挂起函数,返回结果T就是协程的返回结果
 */
public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    val currentThread = Thread.currentThread()
    val contextInterceptor = context[ContinuationInterceptor]
    val eventLoop: EventLoop?
    val newContext: CoroutineContext
    if (contextInterceptor == null) {
        // create or use private event loop if no dispatcher is specified
        eventLoop = ThreadLocalEventLoop.eventLoop
        newContext = GlobalScope.newCoroutineContext(context + eventLoop)
    } else {
        // See if context's interceptor is an event loop that we shall use (to support TestContext)
        // or take an existing thread-local event loop if present to avoid blocking it (but don't create one)
        eventLoop = (contextInterceptor as? EventLoop)?.takeIf { it.shouldBeProcessedFromContext() }
            ?: ThreadLocalEventLoop.currentOrNull()
        newContext = GlobalScope.newCoroutineContext(context)
    }
    val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop)
    coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
    return coroutine.joinBlocking()
}

3.7 MVVM的CoroutineScope

参考:https://developer.android.com/topic/libraries/architecture/coroutines#lifecyclescope

对于MVVM架构,KTX提供了LifecycleOwner、LiveData、ViewModel对应的CoroutineScope;使用方法如下:
(1)添加依赖

dependencies {
	// ViewModelScope
	implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
	// LifecycleScope
	implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
	// liveData
	implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
}

(2)用法示例

// 1.viewModelScope
class MyViewModel: ViewModel() {
    init {
        viewModelScope.launch {
            // Coroutine that will be canceled when the ViewModel is cleared.
        }
    }
}

// 2.lifecycleScope
class MyFragment: Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewLifecycleOwner.lifecycleScope.launch {
            val params = TextViewCompat.getTextMetricsParams(textView)
            val precomputedText = withContext(Dispatchers.Default) {
                PrecomputedTextCompat.create(longTextContent, params)
            }
            TextViewCompat.setPrecomputedText(textView, precomputedText)
        }
    }
}

四、协程启动方式

coroutine builder协程的启动方式有几种:

4.1 CoroutineScope.launch

启动一个协程但不会阻塞调用线程,必须在协程作用域CoroutineScope中才能调用,返回Job表示该新建的协程。

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
}

4.2 CoroutineScope.async

启动一个协程但不会阻塞调用线程,必须在协程作用域CoroutineScope中才能调用。返回Deferred表示该新建的协程。Deferred是Job的子接口。

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
}

CoroutineScope.async与CoroutineScope.launch的区别在于,async方式返回Deferred,可以获取协程的执行结果。

4.3 withContext

指定一个CoroutineContext,并启动一个协程,使用方式如下:

GlobalScope.launch {
	withContext(Dispatchers.Default) {
	}

withContext是一个挂起函数,只能在其他协程或挂起函数内调用:

public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
        // compute new context
        val oldContext = uCont.context
        val newContext = oldContext + context
        // always check for cancellation of new context
        newContext.checkCompletion()
        // FAST PATH #1 -- new context is the same as the old one
        if (newContext === oldContext) {
            val coroutine = ScopeCoroutine(newContext, uCont)
            return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
        }
        // FAST PATH #2 -- the new dispatcher is the same as the old one (something else changed)
        // `equals` is used by design (see equals implementation is wrapper context like ExecutorCoroutineDispatcher)
        if (newContext[ContinuationInterceptor] == oldContext[ContinuationInterceptor]) {
            val coroutine = UndispatchedCoroutine(newContext, uCont)
            // There are changes in the context, so this thread needs to be updated
            withCoroutineContext(newContext, null) {
                return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
            }
        }
        // SLOW PATH -- use new dispatcher
        val coroutine = DispatchedCoroutine(newContext, uCont)
        coroutine.initParentJob()
        block.startCoroutineCancellable(coroutine, coroutine)
        coroutine.getResult()
    }
}

4.4 coroutineScope{}

如3.2所示,coroutineScope{}会创建一个CoroutineScope{},并执行{}内新建的协程,不会阻塞当前线程。

4.5 runBlocking{}

启动一个新的协程并阻塞调用它的线程,会把挂起函数的结果作为协程的结果返回,常用来main方法和测试(所以Android开发中不常用)。

4.6 produce<> { }

启动一个新的协程,并生产一系列数据到channel中,这个协程返回ReceiveChannel,其他协程可从ReceiveChannel中接受数据。

五、CoroutineContext

5.1 定义

CoroutineContext是一系列元素的集合,主要的元素是代表协程的Job,此外还有协程的dispatcher等(Job、Dispatchers与CoroutineName都实现了Element接口)。
CoroutineScope封装了CoroutineContext:

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

5.2 继承性

当通过CoroutineScope.launch启动一个新的协程时,新的CoroutineScope继承了外面的CoroutineContext,并且新的协程Job成为了父协程的子Job,这样当父Job取消时会递归取消子Job。

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
}

以下两种情况不会取消Job除外:
(1)GlobalScope.launch启动的协程;
(2)launch中传递了特定的父Job;

5.3 组合CoroutineContext

一些情况下,我们需要自定义CoroutineContext中的元素,可以使用+来组合CoroutineContext中的各个元素:

launch(Dispatchers.Default + CoroutineName("test")) {
    println("I'm working in thread ${Thread.currentThread().name}")
}

六、CoroutineStart

6.1 CoroutineStart.DEFAULT

CoroutineScope.launch()方式创建协程的启动方式默认为CoroutineStart.DEFAULT,即立即执行。

6.2 CoroutineStart.LAZY

通过设置CoroutineStart.LAZY的协程不会立即执行,需要手动调用start()后才开始执行。可通过如下方式设置启动方式为CoroutineStart.LAZY:

val job = launch(start = CoroutineStart.LAZY) {
	// ...
}
// 手动调用start
job.start()

七、Job

CoroutineScope.launch 函数返回的是一个 Job 对象,代表一个异步的任务。可通过Job的start()、cancle()、join()等方法控制该协程的执行。

7.1 简单示例

val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
    var nextPrintTime = startTime
    var i = 0
    while (isActive) { // cancellable computation loop
        // print a message twice a second
        if (System.currentTimeMillis() >= nextPrintTime) {
            println("job: I'm sleeping ${i++} ...")
            nextPrintTime += 500L
        }
    }
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")```
输出结果如下:
```shell
Hello
World!
Done

可见,等job执行完成后,才会继续输出"Done"。

7.2 获取Job

一个CoroutineContext中只有一个Job,可通过如下方式获取当前CoroutineContext中的Job:

println("My job is ${coroutineContext[Job]}")

输出结果是:

My job is "coroutine#1":BlockingCoroutine{Active}@6d311334

7.3 Job状态

Job 具有生命周期并且可以取消。 Job 还可以有层级关系,一个Job可以包含多个子Job,当父Job被取消后,所有的子Job也会被自动取消;当子Job被取消或者出现异常后父Job也会被取消。

/**
 * A background job.
 * 
 * ### Job实例化方法
 * The most basic instances of `Job` interface are created like this:
 * (1)Coroutine job is created with [launch][CoroutineScope.launch] coroutine builder.
 *   It runs a specified block of code and completes on completion of this block.
 * (2)CompletableJob is created with a `Job()` factory function.
 *   It is completed by calling [CompletableJob.complete].
 * 
 * ### Job states Job状态
 * A job has the following states:
 *
 * | **State**                        | [isActive] | [isCompleted] | [isCancelled] |
 * | -------------------------------- | ---------- | ------------- | ------------- |
 * | _New_ (optional initial state)   | `false`    | `false`       | `false`       |
 * | _Active_ (default initial state) | `true`     | `false`       | `false`       |
 * | _Completing_ (transient state)   | `true`     | `false`       | `false`       |
 * | _Cancelling_ (transient state)   | `false`    | `false`       | `true`        |
 * | _Cancelled_ (final state)        | `false`    | `true`        | `true`        |
 * | _Completed_ (final state)        | `false`    | `true`        | `false`       |
 *
 */

八、挂起函数

suspend修饰的函数叫挂起函数,只能在协程内部或者另一个挂起函数内调用,如下所示;

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // pretend we are doing something useful here
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // pretend we are doing something useful here, too
    return 29
}

8.1 顺序执行

挂起函数是顺序执行的,如下所示:

val time = measureTimeMillis {
    val one = doSomethingUsefulOne()
    val two = doSomethingUsefulTwo()
    println("The answer is ${one + two}")
}
println("Completed in $time ms")

结果如下:

The answer is 42
Completed in 2017 ms

8.2 异步执行

使用sync来异步执行挂起函数,代码如下:

val time = measureTimeMillis {
    val one = async { doSomethingUsefulOne() }
    val two = async { doSomethingUsefulTwo() }
    println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")

结果如下:

The answer is 42
Completed in 1017 ms

8.3 延迟异步执行

val time = measureTimeMillis {
    val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
    val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }
    // some computation
    one.start() // start the first one
    two.start() // start the second one
    println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")

结果如下:

The answer is 42
Completed in 1017 ms

九、协程调度

Coroutine使用CoroutineDispatcher来调度协程在哪个线程执行;CoroutineDispatcher实现了CoroutineContext接口,使用方式如下:

GlobalScope.launch {
	launch { // context of the parent, main runBlocking coroutine
    println("main runBlocking      : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(Dispatchers.Unconfined) { // not confined -- will work with main thread
    println("Unconfined            : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(Dispatchers.Default) { // will get dispatched to DefaultDispatcher 
    println("Default               : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(newSingleThreadContext("MyOwnThread")) { // will get its own new thread
    println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
    }

	// 其他启动过协程的方式也可以指定Dispatcher
	async(Dispatchers.Default) {
	}
	withContext(Dispatchers.Default) {
	}
}

运行结果如下:

Unconfined            : I'm working in thread main
Default               : I'm working in thread DefaultDispatcher-worker-1
newSingleThreadContext: I'm working in thread MyOwnThread
main runBlocking      : I'm working in thread main

下面列举了CoroutineDispatcher常见的几种模式:

1 Dispatchers.Default:

launch或aync启动协程时的默认值,内部使用线程池实现;Dispatchers.Default限制处理器上的内核数(2、4、6、8等);

2 Dispatchers.Main

在主线程中执行协程;

3 Dispatchers.IO:

在IO线程执行协程,一般用于执行网络或者I/O操作,与Dispatchers.Default共享线程池;Dispatchers.IO限制最多64个线程;

4 Dispatchers.Unconfined

Dispatchers.Unconfined不能创建新线程,使用这种调度方式的协程的执行、恢复都在当前线程进行;

十、协程间数据通信

协程间数据通信方式:
(1)CoroutineScope.async返回的Deferred表示协程的执行结果,协程间单个value的传递可使用这种方式;
(2)channels用来在协程间传递一系列value。

10.1 channels

Channel类似BlockingQueue,不同的地方在于BlockingQueue.put()及BlockingQueue.take会阻塞,而channel.send()和channel.receive不会阻塞。

val channel = Channel<Int>()
launch {
    // this might be heavy CPU-consuming computation or async logic, we'll just send five squares
    for (x in 1..5) channel.send(x * x)
}
// here we print five received integers:
repeat(5) { println(channel.receive()) }
println("Done!")

运行结果为:

1
4
9
16
25
Done!

The End

欢迎关注我,一起解锁更多技能:BC的掘金主页~💐 BC的CSDN主页~💐💐
请添加图片描述

官方文档:https://developers.google.com/protocol-buffers/
protobuf github:https://github.com/protocolbuffers/protobuf/tree/master/java
官方文档:https://kotlinlang.org/docs/coroutines-overview.html
github地址:https://github.com/Kotlin/kotlinx.coroutines
协程例子:https://play.kotlinlang.org/hands-on/Introduction%20to%20Coroutines%20and%20Channels/01_Introduction

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值