Kotlin(三)协程入门

527 阅读5分钟

「这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战

六、CoroutineBuilder

1、launch

看下 launch 函数的方法签名。launch 是一个作用于 CoroutineScope 的扩展函数,用于在不阻塞当前线程的情况下启动一个协程,并返回对该协程任务的引用,即 Job 对象

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job

launch 函数共包含三个参数:

  1. context。用于指定协程的上下文
  2. start。用于指定协程的启动方式。默认值为 CoroutineStart.DEFAULT,即协程会在声明的同时就立即进入等待调度的状态,即可以立即执行的状态。可以通过将其设置为CoroutineStart.LAZY来实现延迟启动,即懒加载
  3. block。用于传递协程的执行体,即希望交由协程执行的任务

可以看到 launchA 和 launchB 是并行交叉执行的

fun main() = runBlocking {
    val launchA = launch {
        repeat(3) {
            delay(100)
            log("launchA - $it")
        }
    }
    val launchB = launch {
        repeat(3) {
            delay(100)
            log("launchB - $it")
        }
    }
}
[main] launchA - 0
[main] launchB - 0
[main] launchA - 1
[main] launchB - 1
[main] launchA - 2
[main] launchB - 2

2、Job

Job 是协程的句柄。使用 launchasync 创建的每个协程都会返回一个 Job 实例,该实例唯一标识协程并管理其生命周期。Job 是一个接口类型,这里列举 Job 几个比较有用的属性和函数

    //当 Job 处于活动状态时为 true
    //如果 Job 未被取消或没有失败,则均处于 active 状态
    public val isActive: Boolean
​
    //当 Job 正常结束或者由于异常结束,均返回 true
    public val isCompleted: Boolean
​
    //当 Job 被主动取消或者由于异常结束,均返回 true
    public val isCancelled: Boolean
​
    //启动 Job
    //如果此调用的确启动了 Job,则返回 true
    //如果 Job 调用前就已处于 started 或者是 completed 状态,则返回 false 
    public fun start(): Boolean
​
    //用于取消 Job,可同时通过传入 Exception 来标明取消原因
    public fun cancel(cause: CancellationException? = null)
​
    //阻塞等待直到此 Job 结束运行
    public suspend fun join()
​
    //当 Job 结束运行时(不管由于什么原因)回调此方法,可用于接收可能存在的运行异常
    public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle

Job 具有以下几种状态值,每种状态对应的属性值各不相同

StateisActiveisCompletedisCancelled
New (optional initial state)falsefalsefalse
Active (default initial state)truefalsefalse
Completing (transient state)truefalsefalse
Cancelling (transient state)falsefalsetrue
Cancelled (final state)falsetruetrue
Completed (final state)falsetruefalse
fun main() {
    //将协程设置为延迟启动
    val job = GlobalScope.launch(start = CoroutineStart.LAZY) {
        for (i in 0..100) {
            //每循环一次均延迟一百毫秒
            delay(100)
        }
    }
    job.invokeOnCompletion {
        log("invokeOnCompletion:$it")
    }
    log("1. job.isActive:${job.isActive}")
    log("1. job.isCancelled:${job.isCancelled}")
    log("1. job.isCompleted:${job.isCompleted}")
​
    job.start()
​
    log("2. job.isActive:${job.isActive}")
    log("2. job.isCancelled:${job.isCancelled}")
    log("2. job.isCompleted:${job.isCompleted}")
​
    //休眠四百毫秒后再主动取消协程
    Thread.sleep(400)
    job.cancel(CancellationException("test"))
​
    //休眠四百毫秒防止JVM过快停止导致 invokeOnCompletion 来不及回调
    Thread.sleep(400)
​
    log("3. job.isActive:${job.isActive}")
    log("3. job.isCancelled:${job.isCancelled}")
    log("3. job.isCompleted:${job.isCompleted}")
}
[main] 1. job.isActive:false
[main] 1. job.isCancelled:false
[main] 1. job.isCompleted:false
[main] 2. job.isActive:true
[main] 2. job.isCancelled:false
[main] 2. job.isCompleted:false
[DefaultDispatcher-worker-2] invokeOnCompletion:java.util.concurrent.CancellationException: test
[main] 3. job.isActive:false
[main] 3. job.isCancelled:true
[main] 3. job.isCompleted:true

3、async

看下 async 函数的方法签名。async 也是一个作用于 CoroutineScope 的扩展函数,和 launch 的区别主要就在于:async 可以返回协程的执行结果,而 launch 不行

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T>

通过await()方法可以拿到 async 协程的执行结果,可以看到两个协程的总耗时是远少于七秒的,总耗时基本等于耗时最长的协程

fun main() {
    val time = measureTimeMillis {
        runBlocking {
            val asyncA = async {
                delay(3000)
                1
            }
            val asyncB = async {
                delay(4000)
                2
            }
            log(asyncA.await() + asyncB.await())
        }
    }
    log(time)
}
[main] 3
[main] 4070

由于 launch 和 async 仅能够在 CouroutineScope 中使用,所以任何创建的协程都会被该 scope 追踪。Kotlin 禁止创建不能够被追踪的协程,从而避免协程泄漏

4、async 的错误用法

修改下上述代码,可以发现两个协程的总耗时就会变为七秒左右

fun main() {
    val time = measureTimeMillis {
        runBlocking {
            val asyncA = async(start = CoroutineStart.LAZY) {
                delay(3000)
                1
            }
            val asyncB = async(start = CoroutineStart.LAZY) {
                delay(4000)
                2
            }
            log(asyncA.await() + asyncB.await())
        }
    }
    log(time)
}
[main] 3
[main] 7077

会造成这不同区别是因为 CoroutineStart.LAZY不会主动启动协程,而是直到调用async.await()或者async.satrt()后才会启动(即懒加载模式),所以asyncA.await() + asyncB.await()会导致两个协程其实是在顺序执行。而默认值 CoroutineStart.DEFAULT 参数会使得协程在声明的同时就被启动了(实际上还需要等待被调度执行,但可以看做是立即就执行了),所以即使 async.await()会阻塞当前线程直到协程返回结果值,但两个协程其实都是处于运行状态,所以总耗时就是四秒左右

此时可以通过先调用start()再调用await()来实现第一个例子的效果

asyncA.start()
asyncB.start()
log(asyncA.await() + asyncB.await())

5、async 并行分解

suspend 函数启动的所有协程都必须在该函数返回结果时停止,因此你可能需要保证这些协程在返回结果之前完成。借助 Kotlin 中的结构化并发机制,你可以定义用于启动一个或多个协程的 coroutineScope。然后,你可以使用 await()(针对单个协程)或 awaitAll()(针对多个协程)保证这些协程在从函数返回结果之前完成

例如,假设我们定义一个用于异步获取两个文档的 coroutineScope。通过对每个延迟引用调用 await(),我们可以保证这两项 async 操作在返回值之前完成:

    suspend fun fetchTwoDocs() =
        coroutineScope {
            val deferredOne = async { fetchDoc(1) }
            val deferredTwo = async { fetchDoc(2) }
            deferredOne.await()
            deferredTwo.await()
    }

你还可以对集合使用 awaitAll(),如以下示例所示

suspend fun fetchTwoDocs() =        // called on any Dispatcher (any thread, possibly Main)
    coroutineScope {
        val deferreds = listOf(     // fetch two docs at the same time
            async { fetchDoc(1) },  // async returns a result for the first doc
            async { fetchDoc(2) }   // async returns a result for the second doc
        )
        deferreds.awaitAll()        // use awaitAll to wait for both network requests
}

虽然 fetchTwoDocs() 使用 async 启动新协程,但该函数使用 awaitAll() 等待启动的协程完成后才会返回结果。不过请注意,即使我们没有调用 awaitAll()coroutineScope 构建器也会等到所有新协程都完成后才恢复名为 fetchTwoDocs 的协程。此外,coroutineScope 会捕获协程抛出的所有异常,并将其传送回调用方

6、Deferred

async 函数的返回值是一个 Deferred 对象。Deferred 是一个接口类型,继承于 Job 接口,所以 Job 包含的属性和方法 Deferred 都有,其主要就是在 Job 的基础上扩展了 await()方法