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
}

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

相关推荐
xvch2 小时前
Kotlin 2.1.0 入门教程(二十五)类型擦除
android·kotlin
有点感觉1 天前
Android级联选择器,下拉菜单
kotlin
zhangphil2 天前
Android Coil3缩略图、默认占位图placeholder、error加载错误显示,Kotlin(1)
android·kotlin
xvch2 天前
Kotlin 2.1.0 入门教程(二十三)泛型、泛型约束、协变、逆变、不变
android·kotlin
xvch4 天前
Kotlin 2.1.0 入门教程(二十四)泛型、泛型约束、绝对非空类型、下划线运算符
android·kotlin
zhangphil4 天前
Android Coil ImageLoader MemoryCache设置Key与复用内存缓存,Kotlin
android·kotlin
mmsx4 天前
kotlin Java 使用ArrayList.add() ,set()前面所有值被 覆盖 的问题
android·开发语言·kotlin
lavins4 天前
android studio kotlin项目build时候提示错误 Unknown Kotlin JVM target: 21
jvm·kotlin·android studio
面向未来_4 天前
JAVA Kotlin Androd 使用String.format()格式化日期
java·开发语言·kotlin
alexhilton4 天前
选择Retrofit还是Ktor:给Android开发者的指南
android·kotlin·android jetpack