Kotlin协程文档【1】——协程入门

文章目录

    • 环境配置
    • Hello, Coroutines
    • 协程作用域概念
    • Suspend简单使用规则
    • 协程作用域创建
    • 声明一个显式的任务
    • 协程轻量化

原文地址:https://kotlinlang.org/docs/coroutines-basics.html

环境配置

Kotlin协程文档【1】——协程入门_第1张图片

如图,使用IDEA作为我们的IDE,用Gradle做依赖管理工具,以便后续引入依赖。JDK版本自由选择,我这里是JDK11.

新建工程后等待gradle构建完成,往build.gradle文件中添加协程依赖,如下代码:

plugins {
    id 'org.jetbrains.kotlin.jvm' version '1.5.0'
}

group 'org.example'
version '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib"
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1'
}

同步一下就可以愉快开始玩耍啦。

Hello, Coroutines

协程在功能上是为了更简单、方便地处理异步逻辑。记住这个功能很重要,后续可以帮助我们理解和使用协程的实战场景。

根据传统习惯,先跑个HelloWorld吧。

fun main() = runBlocking {
    launch {
        delay(1000)
        println("Coroutines")
    }
    println("Hello")
}

输出结果是

Hello
Coroutines

下面来解释里面的具体逻辑。

先讲launch这个东西,看起来像关键字,实际上它是一个方法,接收一个高阶函数作为参数,以lambda的形式简化。如果你熟悉Kotlin的thread应用,那么就可以理解为launch像thread一样,thread{}开了一个线程,而launch{}开了一个协程。具体的任务都在代码块里。当然两者是有很大区别的,具体会后面提到。

言归正传,我们再看一下delay这个函数。在IDE中看到它有一个特殊标记,这表明它是一个挂起函数,什么是挂起函数呢?简单来说就是函数有suspend修饰,后面会再详细提到。

这里可以把delay理解为Thread中的sleep方法,对协程进行休眠。

最后看看runBlocking这个函数。如果把这个函数去掉了会怎么样呢?

launch就会提示找不到对应方法了。实际上,查看lauch函数的源码可知,这是一个CoroutineScope对象的扩展函数。而runBlocking方法在代码块内提供了这样一个CoroutineScope对象,launch的实际写法是this.launch{},这样就不难理解了。

一定要有这样一个CoroutineScope对象,我们利用这个对象的launch方法才能启动一个协程任务。

这个CoroutineScope对象的获取是多种多样的,这里用的是其中一种runBlocking方法获取,这个方法可以保证里面的协程任务都运行完了才结束该方法,避免出现主线程跑完了,协程任务还在阻塞,输出了结果也看不到的现象。

协程作用域概念

既然上面提到了协程作用域,这里就要展开讲讲了。这是kotlin协程的一个重要概念。Kotlin协程的核心思想是结构化并发。说人话,就是用一个个Scope把这些协程任务统一管理起来。

以前用线程来执行异步任务时,我们需要自己手动控制线程的wait和notify,手动控制线程的取消,这样的来来回回的工作一多就容易出错,异步编程的噩梦由来已久。Kotlin协程就不一样了,他说开发者你自己弄来弄去吃力不讨好,干脆我做一个协调线程的包,怎么做你不用怎么管,做一些调用工作就好了。那当然好啊。但是Kotlin协程具体怎么做呢?实际上就是把一个个协程任务,都让一个小组长来管理,也就是Scope。做协程的任务时有个人替我们看着了,自然就省心了。我们要做的就是管理一下这些包工头(创建、销毁Scope),发点任务下去(编写具体协程逻辑),至于包工头怎么下发,怎么命令下属,怎么调配人力,至少目前我们不关心~

Suspend简单使用规则

下面我们看看suspend的简单用法。

fun main() = runBlocking {
    launch {
        delay(1000)
        println("Coroutines")
    }
    println("Hello")
}

前面提到,launch用来启动一个协程任务。那么随着任务越来越复杂,代码量增多,我们就开始考虑抽成一个函数,写出以下代码。

Kotlin协程文档【1】——协程入门_第2张图片

发现delay方法提示报错了,为什么呢?这里就是前面提到的:delay()是一个挂起函数。挂起函数的使用要满足以下两个规则的其中一个:

  1. 在协程作用域(也就是Scope对象中)调用挂起函数。
  2. 在挂起函数(也就是suspend修饰的函数)中调用挂起函数。

知道了以上原则,很简单,我们让其满足第二条规则,在fun heavyTask()前加上suspend关键字,就不会报错了。

suspend fun heavyTask(){
    delay(1000)
    println("Coroutines")
    // 一百行代码
}

当然我们也可以这样写,在普通函数里新创建一个作用域来调挂起函数:

fun heavyTask() {
    runBlocking {
        delay(1000)
        println("Coroutines")
        // 一百行代码
    }
}

只不过这种做法通过runBlocking新增了一个包工头(Scope)对象,增加了开销而已,语法上是没问题的。

协程作用域创建

前面提到,协程作用域创建是多种多样的,就像同是包工头,你可以通过社招,通过校招,通过亲戚介绍等等途径获得。每个包工头的作用也不一样,有的喜欢让组员五点下班,有的喜欢晚上干活。

前面提到可以用runBlocking获取Scope对象,当然也可以通过另外一个方法,这里介绍另外一个包工头coroutineScope函数。他们的特点都是会等到内部的协程任务执行完成后才会退出函数,区别就在于runBlocking会创建一个循环队列来保证不被中断,这个写过QT或者Java桌面应用或者了解Android底层的同学应该会很熟悉。而coroutineScope是一个挂起函数,是可以挂起的。

看了半天还是不懂?没关系,说实话,这两个模式的区别很难用代码举例,因为在runBlocking里面同时使用launch和coroutineScope,执行顺序是难以预测的。网上有一些例子,我看了半天也没弄懂,总觉得和官方的表述有出入。

这里给出几条关于使用上的总结:

  1. runBlocking是一个比较底层的方法,一般是框架里面在用。偶尔自己调试的时候也能用一下。
  2. coroutineScope更多地是面向普通开发者的,一般用这个

没有分清也问题不大,不要被这两个差异阻挠了后面协程的学习哈。

下面做个小判断,猜一下这段代码输出结果是什么?

fun main() = runBlocking {
    doWorld()
    println("Done")
}

suspend fun doWorld() = coroutineScope { // this: CoroutineScope
    launch {
        delay(2000L)
        println("World 2")
    }
    launch {
        delay(1000L)
        println("World 1")
    }
    println("Hello")
}
输出结果:
Hello
World 1
World 2
Done

runBlocking内没有显式地launch任务,而是使用了一个包工头(coroutineScope),在包工头内部启动了任务。

实际执行就是两个任务启动,然后被delay了,然后就去打印了“Hello”,等到短的时间结束后就打印“World 1”,再等到长的delay结束后就打印“World 2”。都打印完后,该包工头内的任务就做完了,接了了coroutineScope,也就是结束了doWorld()函数,再回到下一步,打印“Done”。

声明一个显式的任务

前面一直讲,launch就是分配一个任务,这可不是为了形象才这样讲,而是launch确实会返回一个Job对象。

fun main() = runBlocking {
    val job = launch { // launch a new coroutine and keep a reference to its Job
        delay(1000L)
        println("World!")
    }
    println("Hello")
    job.join() // wait until child coroutine completes
    println("Done")     
}

调用这个job对象的join方法,就会等待到该任务结束再执行下面的代码。通过这个方法可以显式地控制该任务的执行时机。

协程轻量化

协程是轻量化的,这也是它与线程最大的不同。前面为了理解,说可以把协程暂时理解为加强版的线程。这部分就来看一下两者的不一样。

fun main() = runBlocking {
    repeat(100_000) { // launch a lot of coroutines
        launch {
            delay(5000L)
            print(".")
        }
    }
}

fun OOMMain() {
    repeat(100_000) { // launch a lot of coroutines
        thread {
            Thread.sleep(5000L)
            print(".")
        }
    }
}	

上下两段代码跑一下应该就能看到两者的差异了。协程不会崩,线程这段会出现OOM。

你可能感兴趣的:(多线程,kotlin,android)