美文网首页Android开发
Kotlin协程教程(1):启动

Kotlin协程教程(1):启动

作者: kross | 来源:发表于2019-08-27 22:11 被阅读0次

    协程

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

    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

    相关文章

      网友评论

        本文标题:Kotlin协程教程(1):启动

        本文链接:https://www.haomeiwen.com/subject/ltayectx.html