Coroutine(协程)(一)

2021-03-04 12:17:27 浏览数 (2)

Coroutine是kotlin官方文档上推荐的,个人理解,其实就是一个轻量级的线程库。当然,协程并不是线程.简单来说,线程(thread)的调度是由操作系统负责,线程的睡眠、等待、唤醒的时机是由操作系统控制,开发者无法决定。使用协程,开发者可以自行控制切换的时机,可以在一个函数执行到一半的时候中断执行,让出CPU,在需要的时候再回到中断点继续执行。因为切换的时机是由开发者来决定的,就可以结合业务的需求来实现一些高级的特性。

使用前加依赖

代码语言:javascript复制
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2")

一、基础

我们可以通过如下代码开启一个协程

代码语言:javascript复制
import kotlinx.coroutines.*

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

代码运行结果就是

代码语言:javascript复制
Hello,
World!

其实GlobalScope.launch可以通过Thread来替代,但要注意delay是一个特殊的 挂起函数 ,它不会造成线程阻塞,但是会 挂起 协程,并且只能在协程中使用。 当然,也可以用后面的 runBlocking 。调用了 runBlocking 的主线程会一直 阻塞 直到 runBlocking 内部的协程执行完毕。 当然,可以使用更合乎惯用法的方式重写,使用 runBlocking 来包装 main 函数的执行:

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

延迟一段时间来等待另一个协程运行并不是一个好的选择。让我们显式(以非阻塞方式)等待所启动的后台Job执行结束:

代码语言:javascript复制
import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = GlobalScope.launch { // 启动一个新协程并保持对这个作业的引用
        delay(1000L)
        println("World!")
    }
    println("Hello,")
    job.join() // 等待直到子协程执行结束
}

现在,结果仍然相同,但是主协程与后台作业的持续时间没有任何关系

当我们使用 GlobalScope.launch 时,我们会创建一个顶层协程。虽然它很轻量,但它运行时仍会消耗一些内存资源。如果我们忘记保持对新启动的协程的引用,它还会继续运行。如果协程中的代码挂起了会怎么样(例如,我们错误地延迟了太长时间),如果我们启动了太多的协程并导致内存不足会怎么样? 这个时候我们可以让他们在同一个协程里处理

代码语言:javascript复制
import kotlinx.coroutines.*

fun main() = runBlocking { // this: CoroutineScope
    launch { // 在 runBlocking 作用域中启动一个新协程
        delay(1000L)
        println("World!")
    }
    println("Hello,")
}

我们来将 launch { …… } 内部的代码块提取到独立的函数中。当你对这段代码执行“提取函数”重构时,你会得到一个带有 suspend 修饰符的新函数。 这是你的第一个挂起函数。在协程内部可以像普通函数一样使用挂起函数, 不过其额外特性是,同样可以使用其他挂起函数(如本例中的 delay)来挂起协程的执行。

代码语言:javascript复制
import kotlinx.coroutines.*

fun main() = runBlocking {
    launch { doWorld() }
    println("Hello,")
}

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

协程很轻量

代码语言:javascript复制
import kotlinx.coroutines.*

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

同样的方法用线程来执行可能会报内存不足 全局协程像守护线程 在 GlobalScope中启动的活动协程并不会使进程保活。它们就像守护线程。

代码语言:javascript复制
import kotlinx.coroutines.*

fun main() = runBlocking {
    GlobalScope.launch {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
    }
    delay(1300L) // 在延迟后退出
}

上面的代码只会输出三行就结束了

代码语言:javascript复制
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...

当改成launch以后会全部输出,这个时候最后的delay也就没用了

二、取消与超时

可以通过job.cancel来取消协程

代码语言:javascript复制
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.")
}

输出如下

代码语言:javascript复制
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
main: Now I can quit.

很明显,在cancel之后就不会打印 job: I'm sleeping 了。现在看下面的代码

代码语言:javascript复制
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.")
}

在cancel之后仍然打印 job: I'm sleeping 。为啥呢?因为一段协程代码必须协作才能被取消

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

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

所以,打印没结束就是因为launch内部没有挂起函数。比如delay就是挂起函数.可以在println前面加上delay(1L)就可以让循环停下来。或者用 while (i < 5) 替换为 while (isActive)。isActive 是一个可以被使用在 CoroutineScope中的扩展属性。

另外,我们可以在 finally 中释放资源

代码语言:javascript复制
fun main() = runBlocking {
    val job = launch {
        try {
            repeat(1000) { i ->
                println("job: I'm sleeping $i ...")
                delay(500L)
            }
        } finally {
            println("job: I'm running finally")
        }
    }
    delay(1300L) // 延迟一段时间
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // 取消该作业并且等待它结束
    println("main: Now I can quit.")
}

但是注意,finally 中不能有挂起函数。任何尝试在 finally 块中调用挂起函数的行为都会抛出 CancellationException。当你需要挂起一个被取消的协程,你可以将相应的代码包装在 withContext(NonCancellable) {……} 中,如下所示:

代码语言:javascript复制
fun main() = runBlocking {
    val job = launch {
        try {
            repeat(1000) { i ->
                println("job: I'm sleeping $i ...")
                delay(500L)
            }
        } finally {
            withContext(NonCancellable) {
                println("job: I'm running finally")
                delay(1000L)
                println("job: And I've just delayed for 1 sec because I'm non-cancellable")
            }
        }
    }
    delay(1300L) // 延迟一段时间
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // 取消该作业并等待它结束
    println("main: Now I can quit.")
}

输出如下

代码语言:javascript复制
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
job: I'm running finally
job: And I've just delayed for 1 sec because I'm non-cancellable
main: Now I can quit.

现在一个很重要的问题就是 CancellationException 好像并没有在控制台显式的展示出来。下面将展示一个显式展示的例子

代码语言:javascript复制
fun main() = runBlocking {
    withTimeout(1300L) {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
    }
}

输出如下

代码语言:javascript复制
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms

withTimeout 抛出了 TimeoutCancellationException,它是 CancellationException 的子类。 我们之前没有在控制台上看到堆栈跟踪信息的打印。这是因为在被取消的协程中 CancellationException 被认为是协程执行结束的正常原因。 然而,在这个示例中我们在 main 函数中正确地使用了 withTimeout

由于取消只是一个例外,所有的资源都使用常用的方法来关闭。 如果你需要做一些各类使用超时的特别的额外操作,可以使用类似 withTimeout 的 withTimeoutOrNull 函数,并把这些会超时的代码包装在 try {...} catch (e: TimeoutCancellationException) {...} 代码块中,而 withTimeoutOrNull 通过返回 null 来进行超时操作,从而替代抛出一个异常

代码语言:javascript复制
fun main() = runBlocking {
    val result = withTimeoutOrNull(1300L) {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
    }
    println("Result is $result")//这里result是null
}

三、组合挂起函数

代码语言:javascript复制
import kotlinx.coroutines.*
import kotlin.system.*

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

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

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

这段代码输出

代码语言:javascript复制
The answer is 42
Completed in 2008 ms

如果 doSomethingUsefulOnedoSomethingUsefulTwo 之间没有依赖,并且我们想更快的得到结果,让它们进行 并发 吗?我们可以使用 async 可以帮助我们的地方。

async 返回一个 Deferred —— 一个轻量级的非阻塞 future, 这代表了一个将会在稍后提供结果的 promise。你可以使用 .await() 在一个延期的值上得到它的最终结果, 但是 Deferred 也是一个 Job,所以如果需要的话,你可以取消它。(注意,使用协程进行并发总是显式的)

代码语言:javascript复制
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
}

输出如下

代码语言:javascript复制
The answer is 42
Completed in 1018 ms

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

代码语言:javascript复制
import kotlinx.coroutines.*
import kotlin.system.*

fun main() = runBlocking<Unit> {
    val time = measureTimeMillis {
        val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
        val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }
        // 执行一些计算
        one.start() // 启动第一个
        two.start() // 启动第二个
        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
}

输出其实是一样的

四、协程上下文与调度器

协程调度器可以将协程限制在一个特定的线程执行,或将它分派到一个线程池,亦或是让它不受限地运行。

  • 当调用 launch { …… } 时不传参数,它承袭了当前协程的上下文(以及调度器)。
  • Dispatchers.Unconfined 是一个特殊的调度器且似乎也运行在 main 线程中,但实际上, 它是一种不同的机制,这会在后文中讲到。
  • 当协程在 GlobalScope 中启动时,使用的是由 Dispatchers.Default 代表的默认调度器。 默认调度器使用共享的后台线程池。 所以 launch(Dispatchers.Default) { …… }GlobalScope.launch { …… } 使用相同的调度器。
  • newSingleThreadContext 为协程的运行启动了一个线程。 一个专用的线程是一种非常昂贵的资源。 在真实的应用程序中两者都必须被释放,当不再需要的时候,使用 close 函数,或存储在一个顶层变量中使它在整个应用程序中被重用。

在Android中的用法如下

代码语言:javascript复制
class Activity {
    //MainScope其实是专门为UI创建,默认Dispatchers.Main
    private val mainScope = MainScope()

    fun destroy() {
        mainScope.cancel()
    }

    fun doSomething() {
        // 在示例中启动了 10 个协程,且每个都工作了不同的时长
        repeat(10) { i ->
            mainScope.launch {
                delay((i   1) * 200L) // 延迟 200 毫秒、400 毫秒、600 毫秒等等不同的时间
                println("Coroutine $i is done")
            }
        }
    }
} 

0 人点赞