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
}

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

相关推荐
用户69371750013842 小时前
24.Kotlin 继承:调用超类实现 (super)
android·后端·kotlin
alexhilton5 小时前
借助RemoteCompose开发动态化页面
android·kotlin·android jetpack
QING61819 小时前
Jetpack Compose Brush API 简单使用实战 —— 新手指南
android·kotlin·android jetpack
QING61821 小时前
Jetpack Compose Brush API 详解 —— 新手指南
android·kotlin·android jetpack
鹿里噜哩21 小时前
Spring Authorization Server 打造认证中心(二)自定义数据库表
spring boot·后端·kotlin
用户69371750013841 天前
23.Kotlin 继承:继承的细节:覆盖方法与属性
android·后端·kotlin
Haha_bj1 天前
五、Kotlin——条件控制、循环控制
android·kotlin
Kapaseker1 天前
不卖课,纯干货!Android分层你知多少?
android·kotlin
urkay-2 天前
Android 切换应用语言
android·java·kotlin·iphone·androidx
杀死那个蝈坦2 天前
监听 Canal
java·前端·eclipse·kotlin·bootstrap·html·lua