协程

协程简单的来说,就是用户态的线程。

emmm,还是不明白对吧,那想象一个这样的场景,如果在一个单核的机器上有两个线程需要执行,因为一次只能执行一个线程里面的代码,那么就会出现线程切换的情况,一会需要执行一下线程A,一会需要执行一下线程B,线程切换会带来一些开销。

假设两个线程,交替执行,如下图所示

线程会因为Thread.sleep方法而进入阻塞状态(就是什么也不会执行),这样多浪费资源啊。

能不能将代码块打包成一个个小小的可执行片段,由一个统一的分配器去分配到线程上去执行呢,如果我的代码块里要求sleep一会,那么就去执行别的代码块,等会再来执行我呢。

协程就是这样一个东西,我们作为使用者不需要再去考虑创建一个新线程去执行一坨代码,也不需要关心线程怎么管理。我们需要关心的是,我要异步的执行一坨代码,待会我要拿到它的结果,我要异步的执行很多坨代码,待会我要按某种顺序,或者某种逻辑得到它们的结果。

总而言之,协程是用户态的线程,它是在用户态实现的一套机制,可以避免线程切换带来的开销,可以高效的利用线程的资源。

从代码上来讲,也可以更漂亮的写各种异步逻辑。

这里想再讲讲一个概念,阻塞与非阻塞是什么意思

阻塞与非阻塞

简单来说,阻塞就是不执行了,非阻塞就是一直在执行。
比如

Thread.wait() // 阻塞了// 这里执行不到了

但是,如果

while (true) { // 一直在运行,没有阻塞   i++;}// 这里也执行不到了

runBlocking:连接阻塞与非阻塞的世界

runBlocking是启动新协程的一种方法。

runBlocking启动一个新的协程,并阻塞它的调用线程,直到里面的代码执行完毕。

举个例子

println("aaaaaaaaa ${Thread.currentThread().name}")runBlocking {    for (i in 0..10) {        println("$i ${Thread.currentThread().name}")        delay(100)    }}println("bbbbbbbbb ${Thread.currentThread().name}")

上面代码的输出为:

aaaaaaaaa main0 main1 main2 main3 main4 main5 main6 main7 main8 main9 main10 mainbbbbbbbbb main

emmm,这并没有什么稀奇,所有的代码都在主线程执行,按照顺序来,去掉runBlocking也是一样的嘛。

但是,runBlocking可以指定参数,就可以让runBlocking里面的代码在其他线程执行,但同样可以阻塞外部线程。

println("aaaaaaaaa ${Thread.currentThread().name}")runBlocking(Dispatchers.IO) { // 注意这里    for (i in 0..10) {        println("$i ${Thread.currentThread().name}")        delay(100)    }}println("bbbbbbbbb ${Thread.currentThread().name}")

上面的代码,给runBlocking添加了一个参数,Dispatchers.IO,这样里面的代码块就会执行到其他线程了。

来一起看看效果:

aaaaaaaaa main0 DefaultDispatcher-worker-11 DefaultDispatcher-worker-12 DefaultDispatcher-worker-13 DefaultDispatcher-worker-44 DefaultDispatcher-worker-45 DefaultDispatcher-worker-66 DefaultDispatcher-worker-77 DefaultDispatcher-worker-78 DefaultDispatcher-worker-99 DefaultDispatcher-worker-110 DefaultDispatcher-worker-5bbbbbbbbb main

通过断点在runBlocking里面的代码,查看这个时候,主线程是什么状态,发现它是进入了WAIT态。

当给runBlocking指定Dispatchers参数时,就仿佛是使用了join方法。

val t = thread {    for (i in 0..10) {        println("$i ${Thread.currentThread().name}")        Thread.sleep(100)    }}t.join()

launch:启动一个协程

launch可以启动一个协程,但不会阻塞调用线程,但是launch必须要在协程作用域中才能调用。

fun main() {    launch {        // no, no, no...    }        runBlocking {        launch {            // is ok        }    }}

如果要在非协程作用域调用launch,可以使用GlobalScope.launch。

fun main() {    GlobalScope.launch {        // is ok    }}

同样的launch也是可以传入一个Dispatcher参数来指定它会被分配到什么线程上执行。

此时,大家就会想了,GlobalScope.launch那么方便,是不是只用它就行了?什么时候该用launch,什么时候该用GlobalScope.launch呢?

文档这样说道:GlobalScope.launch会启动一个top-level的协程,它的生命周期将只受到整个应用程序生命周期的限制。

emmmm,那是不是说,普通的launch,它所创建的协程会受到外层的一个作用域的生命周期的影响,而GlobalScope所创建的协程,不收外层的影响。

于是,有了下面的实验

fun main() {    runBlocking(Dispatchers.IO) {        val job = launch { // 外层任务,包裹两个协程            GlobalScope.launch { // 第一个协程                for (i in 0..10) {                    println("GlobalScope $i ${Thread.currentThread().name} -----")                    delay(100)                }            }            launch { // 第二个协程                for (i in 0..10) {                    println("normal launch $i ${Thread.currentThread().name} #####")                    delay(100)                }            }        }        delay(300); // 延迟一会,让第二个协程能执行3次左右        job.cancel() // 将外层任务取消了        delay(2000) // 继续延迟,期望看到GlobalScope能继续运行            }}

看看实验结果

GlobalScope 0 DefaultDispatcher-worker-2 -----normal launch 0 DefaultDispatcher-worker-5 #####GlobalScope 1 DefaultDispatcher-worker-5 -----normal launch 1 DefaultDispatcher-worker-1 #####GlobalScope 2 DefaultDispatcher-worker-5 -----normal launch 2 DefaultDispatcher-worker-3 #####GlobalScope 3 DefaultDispatcher-worker-7 -----GlobalScope 4 DefaultDispatcher-worker-8 -----GlobalScope 5 DefaultDispatcher-worker-8 -----GlobalScope 6 DefaultDispatcher-worker-7 -----GlobalScope 7 DefaultDispatcher-worker-1 -----GlobalScope 8 DefaultDispatcher-worker-3 -----GlobalScope 9 DefaultDispatcher-worker-9 -----GlobalScope 10 DefaultDispatcher-worker-5 -----

如我的预料一样,GlobalScope无法被cancel。

再来看一下文档里面怎么描述的,体会一下:

Global scope is used to launch top-level coroutines which are operating on the whole application lifetime
and are not cancelled prematurely.

接下来,解释一下上面提到的协程作用域的概念。

什么是协程作用域(Coroutine Scope)?

协程作用域是协程运行的作用范围,换句话说,如果这个作用域销毁了,那么里面的协程也随之失效。就好比变量的作用域。

{ // scope start    int a = 100;} // scope endprintln(a); // what is a?

协程作用域也是这样一个作用,可以用来确保里面的协程都有一个作用域的限制。

一个经典的示例就是,比如我们要在Android上使用协程,但是我们不希望Activity销毁了,我的协程还在悄咪咪的干一些事情,我希望它能停止掉。

我们就可以

class MyActivity : AppCompatActivity(), CoroutineScope by MainScope() {    // ....}

这样,里面运行的协程就会随着Activity的销毁而销毁。

launch的返回值:Job

回到launch的话题,launch启动后,会返回一个Job对象,表示这个启动的协程,我们可以方便的通过这个Job对象,取消,等待这个协程。

像这样:

fun main() {    runBlocking(Dispatchers.IO) {        val job1 = launch {            for (i in 0..10) {                println("normal launch $i ${Thread.currentThread().name} #####")                delay(100)            }        }        val job2 = launch {            for (i in 0..10) {                println("normal launch $i ${Thread.currentThread().name} -----")                delay(100)            }        }        job1.join()        job2.join()        println("all job finished")    }}

使用job的join方法,来等待这个协程执行完毕。这个和Thread的join方法语义一样。

async:启动协程的另一种姿势

launch启动一个协程后,会返回一个Job对象,这个Job对象不含有任何数据,它只是表示启动的协程本身,我们可以通过这个Job对象来对协程进行控制。

假设这样一种场景,我需要同时启动两个协程来搞点事,然后它们分别都会计算出一个Int值,当两个协程都做完了之后,我需要将这两个Int值加在一起并输出。

如果使用launch,我们可能要在外层建立一个变量来记录协程的输出数据了,但是使用async,就可以轻松的解决这个问题!

async的返回值依然是个Job对象,但它可以带上返回值。

上面的小需求可以用下面的代码实现:

fun main() {    runBlocking(Dispatchers.IO) {        val job1 = async {            for (i in 0..10) {                println("normal launch $i ${Thread.currentThread().name} #####")                delay(100)            }            10 // 注意这里的返回值        }        val job2 = async {            for (i in 0..10) {                println("normal launch $i ${Thread.currentThread().name} -----")                delay(100)            }            20 // 注意这里的返回值        }        println(job1.await() + job2.await())        println("all job finished")    }}

这里使用了await方法来获取返回值,它会等待协程执行完毕,并将返回值吐出来。

这样上面的代码就是两个协程自己吭哧吭哧弄完之后,各自返回了10和20,外层再将它们加起来。

总结

这篇文章,我大概的讲了一下协程的概念和被发明的初衷,以及在kotlin中,启动协程的基本方法,最后再总结一下,方便快速复习。

进程是一个应用程序的资源管理单元,线程是一个执行单元,但当线程这个执行单元需要切换状态,停止,启动,或者大量启动的时候,就会比较消耗资源。我们需要一个更轻巧,更容易被控制的执行单元,这就是协程啦。

本篇介绍了runBlocking方法,它可以在非协程作用域下创建一个协程作用域,它的名字也很好,阻塞的执行,意味着,它会阻塞它的调用线程,直到它内部都执行完毕。

launch和async都可以在协程作用域下启动协程,launch以Job对象的形式返回协程任务本身,可以通过Job来操作协程,async以Deferred对象的形式返回协程任务,可以获取执行流的返回值。

GlobalScope.launch会创建一个顶层的协程,它只受限于整个应用的生命周期,不建议使用。


如果你喜欢这篇文章,欢迎点赞评论打赏
更多干货内容,欢迎关注我的公众号:好奇码农君