前言

协程作为Kotlin最大的特性,以及在MVVM架构中的广泛应用,咱们不得不对其要有深刻的了解。

因为协程地东西比较多,所以创建了一个协程专栏来记载相关的文章,一起文章的常识点部分来源于 time.geekbang.org/column/intr… 大家有爱好的能够看看原篇文章。

正文

关于许多开发者来说,都听过一句话:“Kotlin的协程,仅仅Java线程的结构”,可是今天站在学习和了解Kotlin思想的角度来看,咱们要彻底忘记这句话

Kotlin作为一门更现代化的言语,咱们能够说协程是它在语法层面相关于Java的新特性,尽管仅仅新特性,可是咱们有必要树立起协程的思想。了解协程中的”挂起和康复“、”结构化并发“等等概念,这些即是Kotlin言语语法层面的语法糖,也是许多言语通用的概念,更是咱们开发的效率利器。

作为结构来说,其底层完成确实是依据线程的,可是这个却不利于咱们深刻了解协程的规划初衷和思想,简略固化咱们的思想,可是依据线程的观念简略让咱们了解协程的完成原理,所以咱们要客观看待新事物。

关于新特性、新结构,咱们了解其规划理念,就比方协程能够大大简化开发者的并发作业;而关于其原理,咱们能够经过Java比照,来了解其完成原理,然后加深了解,做到心里有底

协程的重要性

协程是Kotlin比照Java的最大优势,Kotlin的协程能够极大地简化并发编程和优化软件架构。这儿的简化异步编程主要就体现在协程能够利用挂起函数完成运用同步的代码完成异步的操作(防止回调地狱),而优化软件架构则是能够利用协程的结构化并发特性和许多比方Flow等API来简化复杂的逻辑。

所以学习协程,最直接的价值便是多了一个处理并发编程的办法,让之前用线程完成的逻辑改用协程;

可是在我看来,学习协程最重要的是了解这个结构的规划理念,比方在之前线程中欠好处理的事务,运用协程怎样优化,协程为什么要这样规划;所以要站在Kotlin协程的创建者角度上,来解析其规划理念,构建一套完好的常识系统,树立一个具体的协程思想模型,提高咱们的架构思想高度。

什么是协程

首要协程是一个十分早的概念,并且在其他许多言语中都有,Kotlin也是最近几年才支撑的,所以咱们先从广义上说,运用简略的言语来描绘协程便是:相互协作的程序

能够看出这儿和一般程序不同的点便是能够相互协作,那怎样相互协作呢 咱们来举个比方看一下。

协程(Coroutine)和一般程序(Routine)差异

这儿举个比方来阐明一般的程序(Routine)和协程(Coroutine)之间的差异,比方下面代码:

fun main() {
    val list = getList()
    printList(list)
}
fun getList(): List<Int> {
    val list = mutableListOf<Int>()
    println("Add 1")
    list.add(1)
    println("Add 2")
    list.add(2)
    println("Add 3")
    list.add(3)
    println("Add 4")
    list.add(4)
    return list
}
fun printList(list: List<Int>) {
    val i = list[0]
    println("Get$i")
    val j = list[1]
    println("Get$j")
    val k = list[2]
    println("Get$k")
    val m = list[3]
    println("Get$m")
}

这儿先调用getList()办法回来一个list,再调用printList()办法打印其中的值,依据Java运转在JVM中规矩来说,这儿会接连创建栈帧,然后调用完出栈,所以这儿的打印肯定是顺序的,成果打印如下:

image.png

这便是一个一般的程序,那下面咱们看一下协程的比方,代码如下:

fun main() = runBlocking {
    val sequence = getSequence()
    printSequence(sequence)
}
fun getSequence() = sequence {
    println("Add 1")
    yield(1)
    println("Add 2")
    yield(2)
    println("Add 3")
    yield(3)
    println("Add 4")
    yield(4)
}
fun printSequence(sequence: Sequence<Int>) {
    val iterator = sequence.iterator()
    val i = iterator.next()
    println("Get$i")
    val j = iterator.next()
    println("Get$j")
    val k = iterator.next()
    println("Get$k")
    val m = iterator.next()
    println("Get$m")
}

这段代码中,回来的是一个Sequence,再按照之前的程序思想,咱们会以为仍是4个Add打印完,再打印Get吗 咱们来看一下打印成果:

image.png

会发现这儿是交替履行的,每当在sequenceAdd一个元素,printSequence就会打印一个元素,即getSequence()办法和printSequence()办法是交替履行的,而这种形式,就像是俩个程序在协作相同。

协作特点

早年面2段代码咱们能很显着地看出协程代码运作的特别之处,而这便是和一般程序的最大差异。上面代码的差异能够总结为:

  1. 一般程序在被调用后,只会在末尾的当地回来,并且只会回来一次,比方前面的getList()办法;而协程不受约束,协程的代码能够在任意yield的当地挂起(Suspend)让出履行权,然后比及合适的机遇再康复(Resume),比方前面getSequence()办法和printSequence()办法能够在办法履行中进行挂起和康复。在这个情况下,yield是代表了退让的意思。
  2. 一般程序需求一次性搜集完一切的值,然后一致回来,比方前面的getList()办法;而协程能够每次回来(yield)一个值,这儿的yield不仅有”退让”的意思,还有”产出“的意思,比方前面的代码中yield(1)就表示产出的值为1。

所以,从广义上来说,协程便是相互协作的程序

这儿所说的各种概念,比方”yield(退让、产出)”、”suspend(挂起)”和”resume(康复)”,都是十分重要的概念,咱们需求站在新的视角,就以为这段程序是具有挂起、康复等特性,而不用强行套用Java的线程思想来思考。

Kotlin协程

前面说了广义的协程概念,现在来看看Kotlin中的协程。

协程和协程结构

这儿要留意一下,这2个东西不是相同的,其中协程表示的是程序中被创建的协程,而协程结构则是一个全体的结构

和Kotlin的反射库相似,为了减小标准库的体积,协程库需求独自依赖:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0'

只要加了依赖,Kotlin协程才能够正常运用。

线程和协程

首要能够经过线程和协程的比较,来了解协程是什么,毕竟线程是实在存在的,操作系统经过线程来进行CPU的分时复用,很简略了解,那协程又是什么呢

为了咱们的代码能够打印出协程,能够对Android Studio做如下装备:

image.png

image.png

image.png

经过上面装备,咱们就能够打印出协程名以及Debug调试协程了。

话不多说,咱们先看个发动线程的比方:

fun main() {
    println(Thread.currentThread().name)
    thread {
        println(Thread.currentThread().name)
        Thread.sleep(100)
    }
    Thread.sleep(1000L)
}

这儿咱们创建了一个子线程,所以打印如下:

image.png

然后咱们发动协程来看一下:

fun main() = runBlocking {
    println(Thread.currentThread().name)
    launch {
        println(Thread.currentThread().name)
        delay(100L)
    }
    Thread.sleep(1000L)
}

上面代码咱们发动了2个协程,咱们来看一下打印:

image.png

能够发现协程和线程有点相似,这儿2个协程都是运转在主线程上。

当咱们能经过打印打印出协程名,就有一种感觉是协程再也不是虚无缥缈的东西了,它就和线程相同,所以咱们能够构建一个协程思想模型。

思想模型

从上面的线程和协程比照能够看出,咱们能够把协程当作是一个”愈加轻量的线程”,留意这儿是当作,而不是协程真的便是线程,已然有了这种了解,咱们能够制作出下面的结构:

image.png

一个系统中,有多个进程,而一个进程有多个线程,依据前面的关系,协程能够了解为运转在线程傍边的、愈加轻量的Task

协程的轻量

说道这儿,或许就有人反对了,说协程运转在线程上,怎样可能会轻量呢?这儿仍是要跳出底层是线程完成的思想陷阱,把协程当作是一个实在的东西,结合前面协程的思想模型,咱们仍是来看看比方来了解为何协程愈加轻量。

比方下面代码咱们创建10亿个线程:

fun main() {
    repeat(1000_000_000) {
        thread {
            Thread.sleep(1000000)
        }
    }
    Thread.sleep(10000L)
}

这个代码在大部分机器上都会因为内存不足而退出,运转如下:

image.png

那假如创建10亿个协程呢:

fun main() = runBlocking {
    repeat(1000_000_000) {
        launch {
            delay(1000000)
        }
    }
    delay(10000L)
}

这个代码是不会反常退出的,从这个简略的比方咱们能够印证协程是运转在线程上更轻量的Task

留意,这儿协程的运转不会和某个线程绑定,在某些情况下,协程能够在不同的线程之间切换的,比方下面代码:

fun main() = runBlocking(Dispatchers.IO) {
    repeat(3) {
        launch {
            repeat(3) {
                println(Thread.currentThread().name)
                delay(100)
            }
        }
    }
    delay(5000L)
}

这儿咱们会敞开3个协程,然后每个协程打印3次,咱们来看一下打印:

image.png

会发现协程#2运转的线程发生了切换,这也就验证了,协程不会和某个线程绑定。

思想模型2.0

这样的话,咱们上面那个思想模型就能够优化一下了,协程依旧是运转在线程上更轻量级的Task,可是能够在不同线程间切换

d89e8744663d45635a5125829a9037a9.gif

就比方上图相同,协程能够在不同线程上运转。

现在能够做个末节:

  1. 协程,能够了解为愈加轻量的线程,不计其数的协程能够一起运转在一个线程中。
  2. 协程,其实便是运转在线程傍边的Task
  3. 协程,不会和特定的线程绑定,它能够在不同的线程之间灵活切换。

非堵塞

说起协程,就不得不说其大名鼎鼎的非堵塞的特性了,关于线程咱们十分熟悉,比方通用的线程生命周期模型中,线程就有休眠这个状况,或许在Java线程生命周期模型中,线程能够经过sleep进行堵塞或许在等待锁的条件变量时进行堵塞,当线程堵塞时,则阐明使命无法继续履行;在Android中尤为显着,当主线程堵塞时,应用程序会ANR。

咱们先来看个线程休眠的比方:

fun main() {
    repeat(3) {
        Thread.sleep(1000L)
        println("Print-1:${Thread.currentThread().name}")
    }
    repeat(3) {
        Thread.sleep(900L)
        println("Print-2:${Thread.currentThread().name}")
    }
}

这儿会让线程休眠,因为sleep()办法是堵塞的,当调用sleep()办法时,线程将无法继续履行,所以打印成果如下图:

image.png

上面是串行的,那咱们来看看协程有什么不相同,比方下面代码:

fun main() = runBlocking {
    launch {
        repeat(3) {
            delay(1000L)
            println("Print-1:${Thread.currentThread().name}")
        }
    }
    launch {
        repeat(3) {
            delay(900L)
            println("Print-2:${Thread.currentThread().name}")
        }
    }
    delay(3000L)
}

这儿协程的代码履行成果如下:

image.png

会发现coroutine#2coroutine#3是交替履行的,可是他们都是在主线程上。

原因是为啥呢,这是因为delay办法是挂起函数,当在协程#2中调用delay()办法时,协程#2会让出履行权,等待1000ms后再继续履行后边操作,可是这儿在让出履行权时,其他协程是能够作业的,所以协程#3会打印,关于挂起函数,后边文章会细说。

可是假如把这儿的delay换成sleep的话,依旧会堵塞,这就阐明Kotlin协程的非堵塞仅仅言语层面的,这也意味着咱们在协程中要尽量运用delay而不是sleep

挂起和康复

早年面的解说就能够看出协程的非堵塞的完成原理,答案便是挂起和康复,一起这个能力也是协程才有的,一般程序不具备

这儿的做法仍是树立思想模型,比方关于一般的程序,咱们在CPU的角度来看相似于下图:

11.gif

当某个使命发生堵塞行为时,比方sleep,当前的Task就会堵塞后边一切使命的履行,如下图:

22.gif

那协程是怎样经过挂起和康复来完成非堵塞呢 这儿就会存在一个相似调度中心的东西,它会来完成Task使命的履行和调度,如下图:

33.webp

而协程除了有调度中心外,每个协程的Task还会多个”抓手”或许”挂钩”的东西,能够方便咱们对他进行挂起和康复,所以流程会如下图:

44.gif

经过比照能够看出,这儿Task会被挂起,它不会堵塞后边的Task的正常履行。看了这个图之后,再想想前面非堵塞的代码,就很好了解了。

总结

协程十分重要,学习协程的第一步是需求打破原来”线程结构”的思想,树立起新的协程思想。

首要便是把协程当作是一个新东西,比方咱们能够把协程当作轻量的线程,也能够当作是运转在线程中的Task

已然是Task,所以一个协程它能够在不同的线程中运转,能够在线程中切换。

然后便是介绍协程的非堵塞特性,这儿能够运用上面思想模型动图了解,即每个协程Task都有抓手,能够进行挂起和康复

仍是那句话,了解协程思想,至关重要。