Kotlin协程教程(1):启动

协程

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

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

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


Image.png

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

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


Image [2].png

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

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

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

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

阻塞与非阻塞

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

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 main
0 main
1 main
2 main
3 main
4 main
5 main
6 main
7 main
8 main
9 main
10 main
bbbbbbbbb 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 main
0 DefaultDispatcher-worker-1
1 DefaultDispatcher-worker-1
2 DefaultDispatcher-worker-1
3 DefaultDispatcher-worker-4
4 DefaultDispatcher-worker-4
5 DefaultDispatcher-worker-6
6 DefaultDispatcher-worker-7
7 DefaultDispatcher-worker-7
8 DefaultDispatcher-worker-9
9 DefaultDispatcher-worker-1
10 DefaultDispatcher-worker-5
bbbbbbbbb main

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


Image [3].png

当给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 end
println(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会创建一个顶层的协程,它只受限于整个应用的生命周期,不建议使用。

相关阅读


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

所有文章二维码推广图_v2.png

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容