Android - Kotlin 协程Global.launch函数

一、简介

那么什么是协程呢?它其实和线程是有点类似的,可以简单地将它理解成一种轻量级的线程。 要知道,我们之前所学习的线程是非常重量级的,它需要依靠操作系统的调度才能实现不同线 程之间的切换。而使用协程却可以仅在编程语言的层面就能实现不同协程之间的切换,从而大 大提升了并发编程的运行效率。

举一个具体点的例子,比如我们有如下foo()和bar()两个方法:

fun foo() {
 a()
 b()
 c()
}
fun bar() {
 x()
 y()
 z()
}

在没有开启线程的情况下,先后调用foo()和bar()这两个方法,那么理论上结果一定是a()、 b()、c()执行完了以后,x()、y()、z()才能够得到执行。而如果使用了协程,在协程A中去 调用foo()方法,协程B中去调用bar()方法,虽然它们仍然会运行在同一个线程当中,但是在 执行foo()方法时随时都有可能被挂起转而去执行bar()方法,执行bar()方法时也随时都有 可能被挂起转而继续执行foo()方法,最终的输出结果也就变得不确定了。

可以看出,协程允许我们在单线程模式下模拟多线程编程的效果,代码执行时的挂起与恢复完 全是由编程语言来控制的,和操作系统无关。这种特性使得高并发程序的运行效率得到了极大 的提升

二、使用 - Global.launch函数

在app/build.gradle文件当中添加如下依赖库:

dependencies {
 ...
 implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1"
 implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1"
}

如何开启一个协程?最简单的方式就是使用 Global.launch函数

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

三、runBlocking函数

在协程中所有代码都运行完了之后再结束的函数

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

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

四、

虽说现在我们已经能够让代码在协程中运行了,可是好像并没有体会到什么特别的好处。这是 因为目前所有的代码都是运行在同一个协程当中的,而一旦涉及高并发的应用场景,协程相比 于线程的优势就能体现出来了。

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

那么如何才能创建多个协程呢?很简单,使用launch函数就可以了,

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

五、suspend -> coroutineScope

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

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

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

 这样就可以在printDot()函数中调用delay()函数了。

但是,suspend关键字只能将一个函数声明成挂起函数,是无法给它提供协程作用域的。比如 你现在尝试在printDot()函数中调用launch函数,一定是无法调用成功的,因为launch函 数要求必须在协程作用域当中才能调用。

这个问题可以借助coroutineScope函数来解决。coroutineScope函数也是一个挂起函数, 因此可以在任何其他挂起函数中调用。它的特点是会继承外部的协程的作用域并创建一个子协 程,借助这个特性,我们就可以给任意挂起函数提供协程作用域了

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

可以看到,现在我们就可以在printDot()这个挂起函数中调用launch函数了

另外,coroutineScope函数和runBlocking函数还有点类似,它可以保证其作用域内的所 有代码和子协程在全部执行完之前,外部的协程会一直被挂起。

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

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

由此可见,coroutineScope函数确实是将外部协程挂起了,只有当它作用域内的所有代码和 子协程都执行完毕之后,coroutineScope函数之后的代码才能得到运行。

虽然看上去coroutineScope函数和runBlocking函数的作用是有点类似的,但是 coroutineScope函数只会阻塞当前协程,既不影响其他协程,也不影响任何线程,因此是不 会造成任何性能上的问题的。而runBlocking函数由于会挂起外部线程,如果你恰好又在主线 程中当中调用它的话,那么就有可能会导致界面卡死的情况,所以不太推荐在实际项目中使用.

六、更多的作用域构建器

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

前面已经说了,runBlocking由于会阻塞线程,因此只建议在测试环境下使用。而 GlobalScope.launch由于每次创建的都是顶层协程,一般也不太建议使用,除非你非常明确 就是要创建顶层协程。

为什么说不太建议使用顶层协程呢?主要还是因为它管理起来成本太高了。举个例子,比如我 们在某个Activity中使用协程发起了一条网络请求,由于网络请求是耗时的,用户在服务器还没 来得及响应的情况下就关闭了当前Activity,此时按理说应该取消这条网络请求,或者至少不应 该进行回调,因为Activity已经不存在了,回调了也没有意义。

那么协程要怎样取消呢?不管是GlobalScope.launch函数还是launch函数,它们都会返回 一个Job对象,只需要调用Job对象的cancel()方法就可以取消协程了

val job = GlobalScope.launch {
 // 处理具体的逻辑
}
job.cancel()

但是如果我们每次创建的都是顶层协程,那么当Activity关闭时,就需要逐个调用所有已创建协 程的cancel()方法,试想一下,这样的代码是不是根本无法维护?

因此,GlobalScope.launch这种协程作用域构建器,在实际项目中也是不太常用的。下面我 来演示一下实际项目中比较常用的写法:

val job = Job()
val scope = CoroutineScope(job)
scope.launch {
 // 处理具体的逻辑
}
job.cancel()

我们先创建了一个Job对象,然后把它传入CoroutineScope()函数当中,注意这 里的CoroutineScope()是个函数,虽然它的命名更像是一个类。CoroutineScope()函数 会返回一个CoroutineScope对象,这种语法结构的设计更像是我们创建了一个 CoroutineScope的实例,可能也是Kotlin有意为之的。有了CoroutineScope对象之后,就 可以随时调用它的launch函数来创建一个协程了。

现在所有调用CoroutineScope的launch函数所创建的协程,都会被关联在Job对象的作用域 下面。这样只需要调用一次cancel()方法,就可以将同一作用域内的所有协程全部取消,从而大大降低了协程管理的成本。

不过相比之下,CoroutineScope()函数更适合用于实际项目当中,如果只是在main()函数 中编写一些学习测试用的代码,还是使用runBlocking函数最为方便。

七、

协程的内容确实比较多,下面我们还要继续学习。你已经知道了调用launch函数可以创建一个 新的协程,但是launch函数只能用于执行一段逻辑,却不能获取执行的结果,因为它的返回值 永远是一个Job对象。那么有没有什么办法能够创建一个协程并获取它的执行结果呢?当然有, 使用async函数就可以实现。

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

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

事实上,在调用了async函数之后,代码块中的代码就会 立刻开始执行。当调用await()方法时,如果代码块中的代码还没执行完,那么await()方法 会将当前协程阻塞住,直到可以获得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函数串行运行耗时

可以看到,整段代码的运行耗时是2032毫秒,说明这里的两个async函数确实是一种串行的关 系,前一个执行完了后一个才能执行。

但是这种写法明显是非常低效的,因为两个async函数完全可以同时执行从而提高运行效率。 现在对上述代码使用如下的写法进行修改

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

现在整段代码的运行耗时变成了1029毫秒,运行效率的提升显而易见

最后:withContext()函数。withContext() 函数是一个挂起函数,大体可以将它理解成async函数的一种简化版写法,示例写法如下

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

调用withContext()函数之后,会立即执行代码块中的代码,同时将 外部协程挂起。当代码块中的代码全部执行完之后,会将最后一行的执行结果作为 withContext()函数的返回值返回,因此基本上相当于val result = async{ 5 + 5 }.await()的写法。唯一不同的是,withContext()函数强制要求我们指定一个线程参数, 关于这个参数详细说一下。

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

猜你喜欢

转载自blog.csdn.net/m0_59482482/article/details/129954173