当前位置: 首页>后端>正文

Kotlin协程入门

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

什么是协程

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

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

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

挂起函数

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

suspend fun delay(timeMillis: Long) {...}
suspend fun someNetworkCallReturningValue(): SomeType {
 ...
}

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

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

挂起函数都是顺序的

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

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

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

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

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的定义,需要传入的最后一个参数是一个挂起函数,但是它本身不是(阻塞线程):

fun <T> runBlocking(
   ..., 
   block: suspend CoroutineScope.() -> T
): T {
  ...
}

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

“launch”发射后不用管

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

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。这个构造器可以得到异步任务执行的返回值。

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的扩展里面:

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的定义如下:

fun <T> CoroutineScope.async(
    ...
    block: suspend CoroutineScope.() -> T
): Deferred<T> {
    ...
}

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

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

fun main() = runBlocking {
    val deferredResult = async {
        delay(1000L)
        "World!"
    }
    println("Hello, ${deferredResult.await()}")
}

fun main() = runBlocking { 
    launch {
        delay(1000L)
        println("World!")
    }
    println("Hello,")
}

fun main() = runBlocking {
    delay(1000L)
    println("Hello, World!")
}

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

coroutineScope构造器

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

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

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
}


https://www.xamrdz.com/backend/3b61936098.html

相关文章: