【kotlin】- 携程基本使用

简介

随着kotlin不断普及,以其简洁的语法糖,易扩展,空安全,汲取了不同语言的优点等...越来越受到开发者的青睐。刚入kotlin,除了和Java不一样的语法让人难以习惯外,“携程”和“泛型”更是让开发者头疼。接下来由我带大家了解kotlin携程基本使用。

其它文章

【kotlin】- delay函数实现原理
【kotlin】- 携程的执行流程
【kotlin】- 携程的挂起和恢复

创建携程

  • \color{blue}{kotlin中使用Thread}
    如果在kotlin使用Thread创建线程,还在像Java那样new一个Thread对象,似乎缺乏违和感。kotlin提供了直接创建线程的方法。

    fun main() {
        thread(start = true,isDaemon = false){
            println("${treadName()}=====创建一个线程")
        }
    }
    

    输出:

    Thread-0=====创建一个线程
    
    Process finished with exit code 0
    

    是不是比Java的方式要简便许多,isDaemon指定线程是否是守护线程,如果这里指定为true日志是打印不出来的哟,原因可以百度一下守护线程

  • \color{blue}{启动一个全局携程}

    fun main() {
        // CoroutineScope(英文翻译:携程范围,即我们的携程体)
        GlobalScope.launch (CoroutineName("指定携程名字")){
            delay(1000)
            println("${Thread.currentThread().name}======全局携程~")
        }
    }
    

    很简单的一个例子(官方例子main最后调用了sleep延迟函数),运行main,发现在控制台并没有打印协调体中的日志,输出如下:

    Process finished with exit code 0
    

    使用官方例子

    fun main() {
        GlobalScope.launch (CoroutineName("指定携程名字")){
            delay(1000)
            println("${Thread.currentThread().name}======全局携程~")
        }
        Thread.sleep(2000L)
        println("${Thread.currentThread().name}======我是最后的倔犟~")
    }
    

    运行输出如下:

    DefaultDispatcher-worker-1======全局携程~
    main======我是最后的倔犟~
    
    Process finished with exit code 0
    

    解释

    从第打印图可以看出,携程创建了新的线程DefaultDispatcher-worker-1来执行,不在主线程,所以全局携程体和全局携程体外的代码是在不同线程中异步执行的。
    全局携程创建的是守护线程,而主线程不是,所以当进程中所有非守护线程执行完,进程就会退出,守护进程也将不复存在。这就是为什么上面例子不能执行打印代码的原因。

    守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。因此,JVM退出时,不必关心守护线程是否已结束

    kotlin携程创建的线程对象是CoroutineScheduler中Worker内部类,看一下这个内部类的初始化。默认就是守护线程。如果大家想要验证,可以使用jps打印当前执行的Java进程,在用jstack查看进程中相关线程的情况。

    internal inner class Worker private constructor() : Thread() {
       init {isDaemon = true}
    }
    
  • \color{blue}{启动子携程}

    // runBlocking协程构建器将 main 函数转换为协程
    fun main(): Unit = runBlocking {
        launch {
            delay(1000)
            println("${treadName()}======局部携程~")
        }
    }
    

    launch是CoroutineScope的扩展函数,所以必须在携程体内才可以调用。输出如下:

    main======局部携程~
    
    Process finished with exit code 0
    

    runBlocking会阻塞当前线程并且等待,在所有已启动的子协程执行完毕之前不会结束。所以launch启动就是runBlocking子携程,因为launch在runBlocking携程作用域中。在看一个例子:

    fun main(): Unit = runBlocking {
        GlobalScope.launch {
            delay(2000L)
            println("${treadName()}======全局携程")
        }
        // 如果没有下面的代码,上面代码不会执行
        launch {
            delay(1000L)
            println("${treadName()}======局部携程")
        }
    }
    

    输出如下:

    main======局部携程
    
    Process finished with exit code 0
    

    解释

    GlobalScope.launch启动是全局携程,会重新新建一个线程来执行,并不是runBlocking的子携程。所以并不会等待GlobalScope.launch携程体执行完再退出进程。

  • \color{blue}{coroutineScope声明携程作用域}

    suspend fun main() {
        // 声明携程作用域,挂起函数,会释放底层线程用于其他用途,创建一个协程作用域并且在所有已启动子协程执行完毕之前不会结束
        coroutineScope {
            // 在该携程作用域启动携程
            launch {
                delay(3000L)
                println("${treadName()}======才开始学习coroutines")
            }
        }
        println("${treadName()}======最后的倔犟~")
    }
    

    这种方式启动的携程作用域就在coroutineScope内。注意日志线程名字,输出如下:

    DefaultDispatcher-worker-1======才开始学习coroutines
    DefaultDispatcher-worker-1======最后的倔犟~
    
    Process finished with exit code 0
    

    从日志发现,main居然不上在主线程执行的,其实并不是这样,反编译kotlin代码,发现main主入口代码变成这样了RunSuspendKt.runSuspend(new KotlinShareKt$$$main(var0))。其实coroutineScope就是创建一个携程环境。

    在看一个复杂的点的例子

    fun main() = runBlocking { 
        launch {
            delay(2000L)
            println("${treadName()}======Task from runBlocking")
        }
        coroutineScope { // 创建一个协程作用域
            launch {
                delay(1000L)
                println("${treadName()}======Task from nested launch")
            }
    
            delay(100L)
            println("${treadName()}======Task from coroutine scope") // 这一行会在内嵌 launch 之前输出
        }
        println("${treadName()}======scope is over")
    }
    

    输出如下:

    main======Task from coroutine scope
    main======Task from nested launch
    main======scope is over
    main======Task from runBlocking
    
    Process finished with exit code 0
    

    解释

    launch {...}执行了挂起函数delay,而coroutineScope{...}可以看着也是一个子携程体,调用挂起函数delay。而launch {...}和coroutineScope{...}后面的代码谁先执行就要看launch中delay延迟的时间了。

  • \color{blue}{CoroutineScope构建携程}

    fun main() {
        val cs = CoroutineScope(Dispatchers.Default)
        cs.launch {  }
    }
    
  • \color{blue}{withContext在指定携程上下文启动携程}
    使用给定的协程上下文调用指定的挂起块,挂起直到它完成,并返回结果

    fun main() = runBlocking {
        val result = withContext(Dispatchers.Default) {
            delay(3000)
            println("${treadName()}======1")
            30
        }
        println("${treadName()}======$result")
    }
    

    输出如下:

    DefaultDispatcher-worker-1======1
    main======30
    
    Process finished with exit code 0
    
  • \color{blue}{Android在生命周期内启动携程}
    需要引入库

    androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha02
    

    使用:

    lifecycleScope.launch {}
    
  • \color{blue}{携程超时}
    在实践中绝大多数取消一个协程的理由是它有可能超时。 当你手动追踪一个相关 Job的引用并启动了一个单独的协程在延迟后取消追踪,这里已经准备好使用 withTimeout 函数来做这件事。

    withTimeout(1300L) {
       repeat(1000) { i ->
           println("I'm sleeping $i ...")
           delay(500L)
       }
    }
    

    扩展
    由于取消只是一个例外,所有的资源都使用常用的方法来关闭。 如果你需要做一些各类使用超时的特别的额外操作,可以使用类似 withTimeoutwithTimeoutOrNull 函数,并把这些会超时的代码包装在 try {...} catch (e: TimeoutCancellationException) {...} 代码块中,而 withTimeoutOrNull通过返回 null 来进行超时操作,从而替代抛出一个异常。

  • \color{blue}{组合挂起函数}

    suspend fun doSomethingUsefulOne(): Int {
        delay(1000L) // 假设我们在这里做了一些有用的事
        return 13
    }
    suspend fun doSomethingUsefulTwo(): Int {
        delay(1000L) // 假设我们在这里也做了一些有用的事
        return 29
    }
    
    • 默认顺序调用
      val time = measureTimeMillis {
          val one = doSomethingUsefulOne()
          val two = doSomethingUsefulTwo()
          println("The answer is ${one + two}")
      }
      println("Completed in $time ms")
      
    • async 并发
      val time = measureTimeMillis {
        val one = async { doSomethingUsefulOne() }
        val two = async { doSomethingUsefulTwo() }
        println("The answer is ${one.await() + two.await()}")
      }
      println("Completed in $time ms")
      
    • 惰性启动的 async
      可选的,async可以通过将 start 参数设置为 CoroutineStart.LAZY而变为惰性的。 在这个模式下,只有结果通过 await获取的时候协程才会启动,或者在 Job 的 start`函数调用的时候。
      val time = measureTimeMillis {
         val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
         val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }
         // 执行一些计算
         one.start() // 启动第一个
         two.start() // 启动第二个
         println("The answer is ${one.await() + two.await()}")
      }
      println("Completed in $time ms")
      
  • \color{blue}{携程join}
    依然例子先行:

    fun main() = runBlocking{
        val job = GlobalScope.launch { // 启动一个新协程并保持对这个作业的引用
            delay(1000L)
            println("World!")
        }
        println("Hello,")
        job.join() // 等待直到协程执行结束
    }
    

    输出如下:

    Hello,
    World!
    
    Process finished with exit code 0
    

    按照之前的讲解,GlobalScope.launch启动的是全局携程,并不属于runBlocking的子携程,所以runBlocking不会等待该携程执行完毕再退出进程,那为什么这里会等待呢,那这就是join函数的功劳,join作用是挂起协程直到携程执行完成。

  • \color{blue}{携程取消}
    协程的取消是 协作 的。一段协程代码必须协作才能被取消。 所有 kotlinx.coroutines 中的挂起函数都是 可被取消的 。它们检查协程的取消, 并在取消时抛出 CancellationException 然而,如果协程正在执行计算任务,并且没有检查取消的话,那么它是不能被取消的。

    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (i < 5) { // 一个执行计算的循环,只是为了占用 CPU
            // 每秒打印消息两次
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("job: I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L) // 等待一段时间
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // 取消一个作业并且等待它结束
    println("main: Now I can quit.")
    

    打印输出并没在控制台上看到堆栈跟踪信息的打印。这是因为在被取消的协程中 CancellationException 被认为是协程执行结束的正常原因

  • 在 finally 中释放资源

    fun main() = runBlocking {
        val job = launch {
            try {
                repeat(1000) { i ->
                    println("job: I'm sleeping $i ...")
                    delay(500L)
                }
            } finally {
                println("job: I'm running finally")
            }
        }
        delay(1300L) // 延迟一段时间
        println("main: I'm tired of waiting!")
        job.cancelAndJoin() // 取消该作业并且等待它结束
        println("main: Now I can quit.")
    }
    
  • 运行不能取消的代码块
    在前一个例子中任何尝试在 finally 块中调用挂起函数的行为都会抛出 CancellationException,因为这里持续运行的代码是可以被取消的。通常,这并不是一个问题,所有良好的关闭操作(关闭一个文件、取消一个作业、或是关闭任何一种通信通道)通常都是非阻塞的,并且不会调用任何挂起函数。然而,在真实的案例中,当你需要挂起一个被取消的协程,你可以将相应的代码包装在 withContext(NonCancellable) {……} 中,并使用 'withContext'函数以及 NonCancellable上下文。

    val job = launch {
        try {
            repeat(1000) { i ->
                println("job: I'm sleeping $i ...")
                delay(500L)
            }
        } finally {
            withContext(NonCancellable) {
                println("job: I'm running finally")
                delay(1000L)
                println("job: And I've just delayed for 1 sec because I'm non-cancellable")
            }
        }
    }
    delay(1300L) // 延迟一段时间
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // 取消该作业并等待它结束
    println("main: Now I can quit.")
    

结束语

很多例子都是官网的,只是加上一些自己的理解,这篇文章只是带大家快速入门kotlin携程使用,后面会逐步深入,讲解携程的实现原理。

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

推荐阅读更多精彩内容