Kotlin协程了解一下

3,869 阅读7分钟

协程是啥玩意?

官方描述

协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、订阅相关事件、在不同线程(甚至不同机器)上调度执行,而代码则保持如同顺序执行一样简单。

人话版

协程就像非常轻量级的线程。

协程是运行在单线程中的并发程序,避免了多线程并发机制中切换线程时带来的线程上下文切换、线程状态切换、线程初始化上的性能损耗,能大幅度唐提高并发性能

线程是由系统调度的,线程切换或线程阻塞的开销都比较大。而协程依赖于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的,协程是由开发者控制的。所以协程也像用户态的线程,非常轻量级,一个线程中可以创建任意个协程。

协程是跑在线程上的,一个线程可以同时跑多个协程,每一个协程则代表一个耗时任务,我们手动控制多个协程之间的运行、切换,决定谁什么时候挂起,什么时候运行,什么时候唤醒,而不是线程那样交给系统内核来操作去竞争CPU时间片,缺点是本质是个单线程,不能利用到单个CPU的多个核

怎么使用?

添加依赖kotlinx.coroutines

kotlinx.coroutines 是由 JetBrains 开发的功能丰富的协程库。它包含本指南中涵盖的很多启用高级协程的原语,包括 launch、 async 等等。

dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3'
}

当然你肯定需要引入kotlin的

buildscript {
    ext.kotlin_version = '1.3.61'
}

repository {
    jcenter()
}

Hello协程

import kotlinx.coroutines.*

fun main() {
    GlobalScope.launch { // 在后台启动一个新的协程并继续
        delay(1000L) // 非阻塞的等待 1 秒钟(默认时间单位是毫秒)
        println("World!") // 在延迟后打印输出
    }
    println("Hello,") // 协程已在等待时主线程还在继续
    Thread.sleep(2000L) // 阻塞主线程 2 秒钟来保证 JVM 存活
}

delay 是一个特殊的 挂起函数 ,它不会造成线程阻塞,但是会 挂起 协程,并且只能在协程中使用。

你希望阻塞线程还是不阻塞?

调用了runBlocking 的线程会一直阻塞直到 runBlocking 内部的协程执行完毕。

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> { // 开始执行主协程
    GlobalScope.launch { // 在后台启动一个新的协程并继续
        delay(1000L)
        println("World!")
    }
    println("Hello,") // 主协程在这里会立即执行
    delay(2000L)      // 延迟 2 秒来保证 JVM 存活
}

这里的 runBlocking { …… } 作为用来启动顶层主协程的适配器。 我们显式指定了其返回类型 Unit,因为在 Kotlin 中 main 函数必须返回 Unit 类型。

launch创建协程

async 创建带返回值的协程,返回的是 Deferred 类

async 同 launch 区别就是 async 是有返回值的 async 返回的是 Deferred 类型,Deferred 继承自 Job 接口,Job有的它都有,增加了一个方法 await ,这个方法接收的是 async 闭包中返回的值,async 的特点是不会阻塞当前线程,但会阻塞所在协程,也就是挂起

withContext 不创建新的协程,在指定协程上运行代码块

runBlocking 不是 GlobalScope 的 API,可以独立使用,区别是 runBlocking 里面的 delay 会阻塞线程,而 launch 创建的不会

协程很轻量

import kotlinx.coroutines.*

fun main() = runBlocking {
    repeat(100_000) { // 启动大量的协程
        launch {
            delay(1000L)
            print(".")
        }
    }
}

它启动了 10 万个协程,并且在一秒钟后,每个协程都输出一个点。 现在,尝试使用线程来实现。会发生什么?(很可能你的代码会产生某种内存不足的错误)

launch 创建协程, launch是个扩展函数,接受3个参数,前面2个是常规参数,最后一个是个对象式函数

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 的3个参数和返回值

CoroutineContext

可以理解为协程的上下文,在这里我们可以设置 CoroutineDispatcher 协程运行的线程调度器,有 4种线程模式:

Dispatchers.Default

Dispatchers.IO 子线程

Dispatchers.Main 主线程

Dispatchers.Unconfined 没指定,就是在当前线程

不写的话就是 Dispatchers.Default 模式的,或者我们可以自己创建协程上下文,也就是线程池

//newSingleThreadContext 单线程,newFixedThreadPoolContext 线程池
val singleThreadContext = newSingleThreadContext("work")
GlobalScope.launch(singleThreadContext) {

}

CoroutineStart

启动模式,默认是DEAFAULT,也就是创建就启动;还有一个是LAZY,意思是等你需要它的时候,再调用启动

val lazyStart = GlobalScope.launch(start = CoroutineStart.LAZY) {
                RLogUtil.i("启动了")
            }
            delay(1000)
            lazyStart.start()

block

闭包方法体,定义协程内需要执行的操作

返回值

协程构建函数的返回值,可以把返回值job看成协程对象本身,协程的操作方法都在job身上了

job.start() - 启动协程,除了 lazy 模式,协程都不需要手动启动

job.join() - 等待协程执行完毕

job.cancel() - 取消一个协程

job.cancelAndJoin() - 等待协程执行完毕然后再取消

suspend 修饰符

当你对这段代码执行“提取函数”重构时,你会得到一个带有 suspend 修饰符的新函数。 那是你的第一个挂起函数

在协程中表现为标记、切换方法、代码段,协程里使用suspend关键字修饰方法,既该方法可以被协程挂起,没用suspend修饰的方法不能参与协程任务,suspend修饰的方法只能在协程中只能与另一个suspend修饰的方法使用

// 这是你的第一个挂起函数
suspend fun doWorld() {
    delay(1000L)
    println("World!")
}

如何取消

使用job.cancel()取消,也可以使用job.cancelAndJoin() - 等待协程执行完毕然后再取消

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        repeat(1000) { i ->
                println("job: I'm sleeping $i ...")
            delay(500L)
        }
    }
    delay(1300L) // 延迟一段时间
    println("main: I'm tired of waiting!")
    job.cancel() // 取消该作业
    job.join() // 等待作业执行结束
    println("main: Now I can quit.")
}

取消是协作的这里需要注意的是一段协程代码必须协作才能被取消。 所有 kotlinx.coroutines 中的挂起函数都是 可被取消的 。它们检查协程的取消, 并在取消时抛出CancellationException。然而,如果协程正在执行计算任务,并且没有检查取消的话,那么它是不能被取消的

import kotlinx.coroutines.*

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (i < 5) { // 一个执行计算的循环,只是为了占用 CPU
            // 每秒打印消息两次
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("job: I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L) // 等待一段时间
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // 取消一个作业并且等待它结束
    println("main: Now I can quit.")
}
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
job: I'm sleeping 3 ...
job: I'm sleeping 4 ...
main: Now I can quit.

我们有两种方法来使执行计算的代码可以被取消。第一种方法是定期调用挂起函数来检查取消。对于这种目的 yield 是一个好的选择。 另一种方法是显式的检查取消状态

将前一个示例中的 while (i < 5) 替换为 while (isActive) 并重新运行它。结果显示是可以取消的

我们通常使用如下的方法处理在被取消时抛出 CancellationException 的可被取消的挂起函数。比如说,try {……} finally {……} 表达式以及 Kotlin 的 use 函数一般在协程被取消的时候执行它们的终结动作

超时

在实践中绝大多数取消一个协程的理由是它有可能超时。 当你手动追踪一个相关 Job 的引用并启动了一个单独的协程在延迟后取消追踪,这里已经准备好使用 withTimeout 函数来做这件事。

withTimeout 抛出了 TimeoutCancellationException,它是 CancellationException 的子类 来看看示例代码:

import kotlinx.coroutines.*

fun main() = runBlocking {
    withTimeout(1300L) {
        repeat(1000) { i ->
                println("I'm sleeping $i ...")
            delay(500L)
        }
    }
}

使用 async 并发

在概念上,async 就类似于 launch。它启动了一个单独的协程,这是一个轻量级的线程并与其它所有的协程一起并发的工作。不同之处在于 launch 返回一个 Job 并且不附带任何结果值,而 async 返回一个 Deferred —— 一个轻量级的非阻塞 future, 这代表了一个将会在稍后提供结果的 promise。你可以使用 .await() 在一个延期的值上得到它的最终结果, 但是 Deferred 也是一个 Job,所以如果需要的话,你可以取消它。

import kotlinx.coroutines.*
import kotlin.system.*

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

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // 假设我们在这里做了些有用的事
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // 假设我们在这里也做了些有用的事
    return 29
}

她们之间是会同时执行doSomethingUsefulOne()和doSomethingUsefulTwo()不使用的话则是按顺序执行

惰性启动的 async

可选的,async 可以通过将 start 参数设置为 CoroutineStart.LAZY 而变为惰性的。 在这个模式下,只有结果通过 await 获取的时候协程才会启动,或者在 Job 的 start 函数调用的时候。

写到最后

上述内容也只是带你们熟悉一下协程,学习协程可能会费些时间,协程和线程类似但是和线程有很大区别,特别需要注意一下协程内的使用顺序,协程是很轻量级的,非常适合做一些并发任务,协程还是有一些高级操作的,如异步流点(看着有点像RxJava)熟练了协程之后基本就可以替代RxJava,协程三连走起!

大家看到这里也基本熟悉了kotlin协程的使用了,本文大多数内容源自于kotlin官网,关于协程还是一些高级用法,咱们下期再见!

感谢