Kotlin 从零开始(四)

一、协程的定义

在线程中,需要依靠操作系统的调度才能实现不同线程之间的切换。而协程可以理解成一种轻量级的线程,它能在编程语言的层面就能实现不同协程之间的切换,大大提升了并发编程的运行效率。

二、协程的基本用法

Kotlin 并没有将协程纳入标准库的 API 当中,而是以依赖库的形式提供的。

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1"

2.1、Global.launch

创建一个 CoroutinesTest.kt 文件,在其中定义一个 main 函数。首先是开启一个协程最简单的方法,使用 Global.launch 函数。Global.launch 函数可以创建一个协程的作用域,这样传递给 launch 函数的代码块(Lambda 表达式)就是在协程中运行的了。

而在下面的代码中,虽然在代码块中打印了一行日志,实际上运行 main 函数后,并不能成功打印出来。因为 Global.launch 函数每次创建的都是一个顶层协程,这种协程当应用程序结束时也会跟着一起结束。在这里,由于代码块中的代码还没来得及运行,应用程序就结束了。

fun main() {
    GlobalScope.launch {
        println("codes run in coroutine scope")
    }
}

要解决上面的问题,我们让程序延迟一段时间再结束。在这里使用 Thread.sleep() 方法让主线程阻塞 1 秒钟,重新运行程序,日志就可以正常打印了。

fun main() {
    GlobalScope.launch {
        println("codes run in coroutine scope")
    }
    Thread.sleep(1000)
}

如果代码块中的代码在 1 秒内不能运行结束,那么就会被强制中断。在这里往代码块中添加了一个 delay 函数,并在之后又打印了一行日志。和 Thread.sleep 方法不同,delay 函数是一个非阻塞式的挂起函数,它只会挂起当前协程,并不会影响其他协程的运行。而 Thread.sleep 方法会阻塞当前的线程,这样运行在该线程下的所有协程都会被阻塞。注意:delay 函数只能在协程的作用域或其他挂起函数中调用。在这里我们让协程挂起了 1.5 秒,但是主线程只阻塞了 1 秒,可以发现新增的日志并没有打印出来。

fun main() {
    GlobalScope.launch {
        println("codes run in coroutine scope")
        delay(1500)
        println("codes run in coroutine scope finished")
    }
    Thread.sleep(1000)
}

2.2、runBlocking

如果想要应用程序在协程中的所有代码都运行完后再结束,可以使用 runBlocking 函数。runBlocking 函数同样会创建一个协程的作用域,但是它可以保证在协程作用域内的所有代码和子协程没有全部执行完前一直阻塞当前线程。需要注意的是,runBlocking 函数通常只在测试环境下使用,在正式环境使用容易产生性能上的问题。

fun main() {
    runBlocking {
        println("codes run in coroutine scope")
        delay(1500)
        println("codes run in coroutine scope finished")
    }
}

使用 launch 函数还可以创建多个协程。注意这里和 GlobalScope.launch 函数不同。首先它必须在协程的作用域中才能调用,其次他会在当前协程的作用域下创建子协程。子协程的特点是如果外层作用域的协程结束了,该作用域下的所有子协程也会一同结束。相比而言,GlobalScope.launch 函数创建的永远是顶层协程,这一点和线程比较像,因为线程也没有层级这一说,永远都是顶层的。

fun main() {
    runBlocking {
        launch {
            println("launch1")
            delay(1000)
            println("launch1 finished")
        }
        launch {
            println("launch2")
            delay(1000)
            println("launch2 finished")
        }
    }
}

2.3、suspend

随着 launch 函数中的逻辑越来越复杂,可能需要将部分代码提取到一个单独的函数中。这时就产生了一个问题:我们在 launch 函数中编写的代码是拥有协程作用域的,但是提取到一个单独的函数中就没有协程作用域了,就无法调用像 delay 这样的挂起函数了。

为此 Kotlin 提供了一个 suspend 关键字,使用它可以将任意函数声明成挂起函数,而挂起函数之间都是可以互相调用的,这时就可以在 printDot 函数中调用 delay 了。

suspend fun printDot() {
    println(".")
    delay(1000)
}

2.4、coroutineScope

suspend 关键字只能将一个函数声明成挂起函数,却不能给该函数提供协程作用域。如果现在需要在 printDot 函数中调用 launch 函数,是无法调用成功的,因为 launch 函数必须在协程作用域中才能调用。

Kotlin 中提供了一个挂起函数 coroutineScope,挂起函数可以在任何其他挂起函数中调用,比如这里的 printDot 函数。coroutineScope 函数的特点是会继承外部的协程作用域并创建一个子作用域,借助这个特性,我们就可以给任意挂起函数提供协程作用域了。

suspend fun printDot() {
    coroutineScope {
        launch {
            println(".")
            delay(1000)
        }
    }
}

另外,coroutineScope 函数和 runBlocking 函数有相同的作用,它可以保证其作用域内的所有代码和子协程在全部执行完之前,会一直阻塞当前线程。

举一个例子,先使用 runBlocking 函数创建了一个协程作用域,然后又使用 coroutineScope 函数创建了一个子协程作用域。在 coroutineScope 的作用域中,又调用 launch 函数创建了一个子协程,并通过 for 循环打印数字 1 到 10,每次打印间隔 1 秒。最后在 runBlocking 和 coroutineScope 函数的结尾,分别打印了一行日志。

fun main() {
    runBlocking {
        coroutineScope {
            launch {
                for (i in 1..10) {
                    println(i)
                    delay(1000)
                }
            }
        }
        println("coroutineScope finished")
    }
    println("runBlocking finished")
}

在这里插入图片描述
虽然看上去 coroutineScope 函数和 runBlocking 函数相似,但是需要注意,coroutineScope 函数只会阻塞当前协程,即不影响其他协程,也不影响任何线程,因此不会造成性能上的问题。而 runBlocking 函数由于会阻塞当前协程,如果你恰好又在主线程中调用的话,那么就可能导致界面卡死的情况。

三、更多的作用域构建器

前面介绍了 GlobalScop.launch、runBlocking、launch、coroutineScope 这几种作用域构建器,它们都可以用于创建一个新的协程作用域。不过 GlobalScope.launch 和 runBlocking 函数是可以在任意地方调用的,coroutineScope 函数可以在协程作用域或挂起函数中调用,而 launch 函数只能在协程作用域中调用。

runBlocking 由于会阻塞线程,因此只建议在测试环境下使用。GlobalScope.launch 由于每次创建的都是顶层协程,一般也不太建议使用。因为顶层协议的管理成本较高,比如在一个 Activity 中使用协程执行耗时操作,如果用户在未执行完就已经关闭了 Activity,再回调就没有意义了。

3.1、Job

协程是可以取消的,launch 函数会返回一个 Job 对象,只需要调用 Job 对象的 cancel 方法就可以取消协程。

    val job = GlobalScope.launch {
        //do something
    }
    job.cancel()

同时管理多个协程。先创建了一个 Job 对象,然后传入 CoroutineScope 函数中,注意这里的 CoroutineScope 是一个函数。CoroutineScope 函数会返回一个 CoroutineScope 对象,之后所有使用 CoroutineScope 对象的 launch 所创建的协程,都会被关联在 Job 对象的作用域下面。这样只需要调用一次 cancel 方法,就可以将同一作用域内的所有协程取消。

    val job = Job()
    val scope = CoroutineScope(job)
    scope.launch {
        //do something
    }

3.2、async

对于 launch 函数可以创建一个新的协程,但是 launch 函数只能在其中执行逻辑,却不能获取执行的结果。因为它的返回值是一个 Job 对象。如果想要创建一个协程并获取执行结果,可以使用 async 函数。

async 函数必须在协程作用域中才能调用,它会创建一个新的子协程并返回一个 Deferred 对象,如果我们想要获取 async 函数代码块的执行结果,只需要调用 Deferred 对象的 await 函数即可。

fun main() {
    runBlocking {
        val result = async {
            5 + 5
        }.await()
        println(result)
    }
}

事实上,在调用了 async 函数之后,代码块中的代码会立刻开始执行,当调用 await 函数时,如果代码还没执行完,那么 await 函数会将当前协程阻塞住,直到可以获得 async 函数的执行结果。比如下面的代码,两个 async 就是串行的,第一个 async 执行完后才会再执行第二个 async。

fun main() {
    runBlocking {
        val start = System.currentTimeMillis()
        val result1 = async {
            delay(1000)
            5 + 5
        }.await()
        val result2 = async {
            delay(1000)
            4 + 6
        }.await()
        println("result is ${result1 + result2}")
        val end = System.currentTimeMillis()
        println("cost ${end - start} ms")
    }
}

上面的代码,两个 async 函数完全是可以同时执行来提高运行效率的。修改代码, 不是每次调用 async 函数后就立刻使用 await 函数获取结果,而是仅在需要用到 async 函数的执行结果时才调用 await 函数进行获取。

fun main() {
    runBlocking {
        val start = System.currentTimeMillis()
        val result1 = async {
            delay(1000)
            5 + 5
        }
        val result2 = async {
            delay(1000)
            4 + 6
        }
        println("result is ${result1.await() + result2.await()}")
        val end = System.currentTimeMillis()
        println("cost ${end - start} ms")
    }
}

3.3、withContext

withContext 函数是一个挂起函数,大体可以将它理解成 async 函数的一种简化版写法。调用 withContext 函数之后,也是立即执行代码块中的代码,同时将当前协程阻塞住。当代码块中的代码全部执行完之后,会将最后一行的执行结果作为 withContext 函数的返回值返回。

fun main() {
    runBlocking {
        val result = withContext(Dispatchers.Default) {
            5 + 5
        }
        println(result)
    }
}

跟 async 函数不同的是,withContext 需要指定一个线程参数。首先我们知道,协程是一种轻量级的线程的概念,因此在很多传统编程情况下需要开启多线程执行的并发任务,现在只需要在一个线程下开启多个协程就可以了。但是这并不意味着永远不需要开启线程了,比如 Android 中要求网络请求必须在子线程中进行,即使开启了协程执行网络请求,假如这个协程是在主线程中,那么程序依然会出错。这时就应该通过线性参数给协程指定一个具体的运行线程。

  1. Dispatchers.Default :表示会使用一种默认低并发的线程策略。当你要执行的代码属于计算密集型任务时使用,因为开启过高的并发反而可能会影响任务的运行效率。
  2. Dispatchers.IO:表示会使用一种较高并发的线程策略。当你要执行的代码大多时间是在阻塞和等待中时使用,比如网络请求。
  3. Dispatchers.Main:表示不会开启子线程,而是在 Android 主线程中执行代码,但是这个值只能在 Android 项目中使用,纯 Kotlin 程序使用这种类型的线程参数会出现错误。

事实上,在前面所有的协程作用域构建器中,除了 coroutineScope 函数外,其他所有的函数都可以指定这样一个线程参数,只不过对于 withContext 函数是强制要求指定的,而其他函数时可选的。

四、suspendCoroutine

suspendCoroutine 函数必须在协程作用域或挂起函数中才能调用,它接收一个 Lambda 表达式参数,主要作用是将当前协程立即挂起,然后在一个普通线程中执行 Lambda 表达式中的代码。Lambda 表达式的参数列表上会传入一个 Continuation 参数,调用它的 resume 方法或 resumeWithException 可以让协程恢复执行。

五、相关资料

Kotlin协程

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值