Kotlin协程入门

写的太挫,可以不用看先。过段加些示例代码更适合观看

Kotlin提供了协程来更加方便的实现异步代码。主要的部分就是suspend方法以及配套的丰富的API和库。本文尽可能的用简单的语言来解释协程的基础概念。

什么是协程

Kotlin团队把它定义为"轻量级的线程",它们是实际的线程可以执行的某种任务。

线程可以在某个"挂起点"停止执行协程,而去处理其他的任务。然后可以在之后继续执行这个协程,也有可能是在另外一个线程上执行

所以,一个协程不是一个任务,而是一系列的"子任务"。这些"子任务"会按照特定的顺序执行。即使代码看起来是在一个顺序的代码块里的,协程里对"挂起函数"的调用时间也是顺序执行的。这就需要我们一探挂起函数的究竟了。

挂起函数

在kotlinx的delay或者是Ktor的HttpClient.post函数的定义都带有关键字suspend

kotlin 复制代码
suspend fun delay(timeMillis: Long) {...}
suspend fun someNetworkCallReturningValue(): SomeType {
 ...
}

这些函数就叫做挂起函数。挂起函数可以挂起当前协程的执行而不会阻塞所在的线程

也就是说挂起函数可能在某个点停止了执行,而在之后的某个时间点又继续执行。然而这里没有说到当前线程会干什么。

挂起函数都是顺序的

挂起函数并没有什么特别的返回类型。除了多了一个suspend关键字并没有其他特别的地方。也不需要类似于Java的Future或者JavaScript的Promise之类的包装器。这也就更加确定了挂起函数本身并不是异步的(至少从调用者的角度看是这样),也不像JavaScript的async方法需要返回一个promise。

在挂起函数里调用其他的挂起函数和平常的函数调用没什么区别:被调用的函数执行完之后才会继续执行剩下的代码。

kotlin 复制代码
suspend fun someNetworkCallReturningSomething(): Something {
    // some networking operations making use of the suspending mechanism
}

suspend fun someBusyFunction(): Unit {
    delay(1000L)
    println("Printed after 1 second")
    val something: Something = someNetworkCallReturningSomething()
    println("Received $something from network")
}

如此一来复杂的异步代码写起来也就相当容易了。

挂起和非挂起怎么连接到一起

直接在非挂起函数里调用挂起函数是无法编译的。这是因为只有协程里才可以调用挂起函数,所以我们要新建一个协程先。这就需要用到协程构造器:

协程构造器

协程构造器就是新建了一个挂起函数,然后调用其他的挂起函数。他们可以在非挂起函数内被调用,是因为他们本身不是挂起函数,也就可以扮演一个普通函数和挂起函数的桥梁。

Kotlin提供了很多种不同的协程构造器,我们来认识几种:

阻塞当前线程的runBlocking

这是最简单的协程构造器了。它会阻塞当前线程一直等到里面的挂起函数都执行完毕:

kotlin 复制代码
fun main() { 
    println("Hello,")
    
    // we create a coroutine running the provided suspending lambda
    // and block the main thread while waiting for the coroutine to finish its execution
    runBlocking {
        // now we are inside a coroutine
        delay(2000L) // suspends the current coroutine for 2 seconds
    }
    
    // will be executed after 2 seconds
    println("World!")
}

可以看到runBlocking的定义,需要传入的最后一个参数是一个挂起函数,但是它本身不是(阻塞线程):

kotlin 复制代码
fun <T> runBlocking(
   ..., 
   block: suspend CoroutineScope.() -> T
): T {
  ...
}

runBlocking经常用在讲解协程时候的hello world例子里阻塞main方法显示挂起函数执行的结果。

"launch"发射后不用管

一般协程是不阻塞所在的线程的,而是开始一个异步任务。协程构造器launch就是用来在后台开始一个异步任务的。比如:

kotlin 复制代码
fun main() { 
    GlobalScope.launch { // launch new coroutine in background and continue
        delay(1000L)
        println("World!")
    }
    println("Hello,") // main thread continues here immediately
    runBlocking {     // but this expression blocks the main thread
        delay(2000L)  // ... while we delay for 2 seconds to keep JVM alive
    } 
}

这个会打印出"Hello", 随后打印出"World"。

GlobalScope不用急,后面会详细的讲到。

本例中为了可以看到输出的结果所以在最后还是阻塞的了线程。

使用"async"获得异步任务的结果

这是另外一个协程构造器async。这个构造器可以得到异步任务执行的返回值。

kotin 复制代码
fun main() {
    val deferredResult: Deferred<String> = GlobalScope.async {
        delay(1000L)
        "World!"
    }
    
    runBlocking {
        println("Hello, ${deferredResult.await()}")
    }
}

async构造器会返回一个Deferred类型的对象,这和Future或者Promise类似。之后通过await调用可以得到异步任务的返回结果。

await不是一个简单的阻塞方法,它是一个挂起函数。也就是说这个不能直接在mian方法里调用await,所以上例中是在runBlocking里调用的。

这里再次出现了GlobalScope。协程的scope是用来创建结构化的并发的。

结构化并发

从上面的例子里,你会发现他们有个共同点:阻塞并等待协程执行完成。

kotlin可以席间结构化的协程,这样父协程可以管理子协程的生命周期。他可以等待所有的子协程完成,或者一个子协程发生异常的时候取消所有的子协程。

创建结构化的协程

除了runBlocking,一般不在协程里调用,所有的协程构造器都定义在CoroutineScope的扩展里面:

kotlin 复制代码
fun <T> runBlocking(...): T {...}
fun <T> CoroutineScope.async(...): Deferred<T> {...}
fun <T> CoroutineScope.launch(...): Job {...}
fun <E> CoroutineScope.produce(...): ReceiveChannel<E> {...}
...

要新建一个协程,你要么用GlobalScope(新建一个顶层协程),要么用一个已经存在的协程scope的扩展方法。有一个可以说是某种约定,最好是写一个CoroutineScope的扩展方法来新建协程。

async的定义如下:

kotlin 复制代码
fun <T> CoroutineScope.async(
    ...
    block: suspend CoroutineScope.() -> T
): Deferred<T> {
    ...
}

看上面的代码,async传入的参数也是一个CoroutineScope的扩展。也就是说你可以在里面调用协程构造器而不用指定调用对象。

上面的例子的可以这样修改:

kotlin 复制代码
fun main() = runBlocking {
    val deferredResult = async {
        delay(1000L)
        "World!"
    }
    println("Hello, ${deferredResult.await()}")
}
kotlin 复制代码
fun main() = runBlocking { 
    launch {
        delay(1000L)
        println("World!")
    }
    println("Hello,")
}
kotlin 复制代码
fun main() = runBlocking {
    delay(1000L)
    println("Hello, World!")
}

我们不再需要GlobalScope了,因为runBlocking已经提供了一个scope了。

coroutineScope构造器

上面说过runBlocking不鼓励使用。因为Kotlin的协程的初衷就是不阻塞线程。不过runBlocking就是一个coroutineScope构造器。

coroutineScope会挂起了当前的协程,一直到所有的子协程都执行完毕。例如:

kotlin 复制代码
fun main() = runBlocking { // this: CoroutineScope
    launch { 
        delay(200L)
        println("Task from runBlocking")
    }
    
    coroutineScope { // Creates a new coroutine scope
        launch {
            delay(500L) 
            println("Task from nested launch")
        }
    
        delay(100L)
        println("Task from coroutine scope") // This line will be printed before nested launch
    }
    
    println("Coroutine scope is over") // This line is not printed until nested launch completes
}

写的不咋地,凑合看吧。。

相关推荐
陆业聪32 分钟前
基本数据类型:Kotlin、Dart (Flutter)、Java 和 C++ 的比较
java·flutter·kotlin
android_cai_niao35 分钟前
Kotlin中泛型的协变
kotlin·协变·kotlin协变
卡卡_R-Python16 小时前
子集选择——基于R语言实现(最优子集选择法、逐步回归法、Lasso回归法、交叉验证法)
回归·r语言·kotlin
夏非夏3 天前
Kotlin jetpack MVP
android·kotlin
zhangphil3 天前
Kotlin约束泛型参数必须继承自某个父类
kotlin
ch_kexin3 天前
Android kotlin integer-array 存放图片资源ID
android·开发语言·kotlin
jiay24 天前
Kotlin-面向对象之构造函数、实例化和初始化
android·开发语言·kotlin
我怀里的猫4 天前
glide ModelLoader的Key错误使用 可能造成的内存泄漏
android·kotlin·glide
陟彼高冈yu4 天前
第10天:Fragments(碎片)使用-补充材料——‘MainActivity.kt‘解读
android·kotlin·android studio
姑苏风4 天前
《Kotlin实战》-第11章:DSL构建
android·开发语言·kotlin