内容提要:
- 协程是什么
- 协程怎么用
- 协程有什么优点
-----------------------------------------------------------------------------------------------
正文:
一、协程是什么
本质上,协程是轻量级的线程。
二、协程怎么用
1. 如何启动协程
【方法1】:用launch启动协程
import kotlinx.coroutines.*
fun main() {
GlobalScope.launch { // 在后台启动一个新的协程并继续
delay(1000L) // 非阻塞的等待 1 秒钟(默认时间单位是毫秒)
println("World!") // 在延迟后打印输出
}
println("Hello,") // 协程已在等待时主线程还在继续
Thread.sleep(2000L) // 阻塞主线程 2 秒钟来保证 JVM 存活
}
输出:
Hello,
World!
【方法2】用runBlocking协程构建器启动协程
import kotlinx.coroutines.*
fun main() = runBlocking { // this: CoroutineScope
launch { // 在 runBlocking 作用域中启动一个新协程
delay(1000L)
println("World!")
}
println("Hello,")
}
输出:
Hello,
World!
说明:
- 协程出现了层级关系:runBlocking修饰的协程和launch修饰的协程,后者包含于前者之内,称之为前者的子协程。
- 外部协程会等待子协程都执行完毕后才会结束。
【方法3】:用coroutineScope构建器启动协程——作用域构建器
import kotlinx.coroutines.*
fun main() = runBlocking { // this: CoroutineScope
launch {
delay(200L)
println("Task from runBlocking")
}
coroutineScope { // 创建一个协程作用域
launch {
delay(500L)
println("Task from nested launch")
}
delay(100L)
println("Task from coroutine scope") // 这一行会在内嵌 launch 之前输出
}
println("Coroutine scope is over") // 这一行在内嵌 launch 执行完毕后才输出
}
输出:
Task from coroutine scope
Task from runBlocking
Task from nested launch
Coroutine scope is over
说明:
- coroutineScope跟runBlocking一样,会等待子协程结束。
- runBlocking会阻塞当前纯种来等待,而coroutineScope只是挂起,会释放底层纯种用于其他用途。
- runBlocking是常规函数,而coroutineScope是挂起函数。
- GlobalScope是一个全局协程,因此用GlobalScope.lanch启动的子协程的生命周期只受整个应用的生命周期限制。
2.什么是挂起函数
可以把launch{...}内部的代码块提取到独立的函数中
import kotlinx.coroutines.*
fun main() = runBlocking {
launch { doWorld() }
println("Hello,")
}
// 这是你的第一个挂起函数
suspend fun doWorld() {
delay(1000L) //调用其他挂起函数
println("World!")
}
说明:
- 提取出来的函数要用suspend修饰,称为挂起函数
- 挂起函数内也可以调用其他挂起函数,如delay()
3. 如何取消协程
一个用户也许关闭了一个启动了协程的界面,那么现在协程的执行结果已经不再被需要了,这时,它应该是可以被取消的。
val job = launch {
repeat(1000) { i ->
println("job: I'm sleeping $i ...")
delay(500L)
}
}
delay(1300L) // 延迟一段时间
println("main: I'm tired of waiting!")
job.cancel() // 取消该作业
job.join() // 等待作业执行结束
println("main: Now I can quit.")
输出:
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
main: Now I can quit.
说明:
- launch函数返回了一个可以被用来取消运行中的协程的job
- 通过job.cancel()取消作业
- 通过job.join()等待执行结束
- job.cancelAndJoin() = job.cancel() + job.join()
4.协程的超时
withTimeout(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
说明:
- 超时后为抛出一个TimeoutCancellationException异常
val result = withTimeoutOrNull(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
"Done" // 在它运行得到结果之前取消它
}
println("Result is $result")
说明:
- 超时后返回null
5. 组合挂起函数
先定义两个函数:
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // 假设我们在这里做了一些有用的事
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // 假设我们在这里也做了一些有用的事
return 29
}
a)【默认顺序】
val time = measureTimeMillis {
val one = doSomethingUsefulOne()
val two = doSomethingUsefulTwo()
println("The answer is ${one + two}")
}
println("Completed in $time ms")
输出:
The answer is 42
Completed in 2006 ms
b)【使用 async 并发】
val time = measureTimeMillis {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")
输出:
The answer is 42
Completed in 1029 ms
说明:
doSomethingUsefulOne和doSomethingUsefulTwo没有依赖,通过async实现并发执行。
- async类似于launch,它启动了一个单独的协程。
- 不同之处:launch返回一个job并且不附带任何结果,而async返回一个Deferred——一个轻量级的非阻塞future,这代表了一个将会在稍后提供结果的承诺。
- 你可以使用
.await()
在一个延期的值上得到它的最终结果。 Deferred
也是一个job,因此也可以被取消。
c)【惰性启动的async】
val time = measureTimeMillis {
val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }
// 执行一些计算
one.start() // 启动第一个
two.start() // 启动第二个
println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")
输出:
The answer is 42
Completed in 1021 ms
说明:
- async通过start = CoroutineStart.LAZY变为惰性的
- 这个模式下,只有结果通过await获取的时候协程才会启动,或者在job的start函数调用的时候。
- 如果我们只是在
println
中调用 await,而没有在单独的协程中调用 start,这将会导致顺序行为,直到 await 启动该协程 执行并等待至它结束。
d)【async风格的函数】
// somethingUsefulOneAsync 函数的返回值类型是 Deferred<Int>
fun somethingUsefulOneAsync() = GlobalScope.async {
doSomethingUsefulOne()
}
// somethingUsefulTwoAsync 函数的返回值类型是 Deferred<Int>
fun somethingUsefulTwoAsync() = GlobalScope.async {
doSomethingUsefulTwo()
}
// 注意,在这个示例中我们在 `main` 函数的右边没有加上 `runBlocking`
fun main() {
val time = measureTimeMillis {
// 我们可以在协程外面启动异步执行
val one = somethingUsefulOneAsync()
val two = somethingUsefulTwoAsync()
// 但是等待结果必须调用其它的挂起或者阻塞
// 当我们等待结果的时候,这里我们使用 `runBlocking { …… }` 来阻塞主线程
runBlocking {
println("The answer is ${one.await() + two.await()}")
}
}
println("Completed in $time ms")
}
输出:
The answer is 42
Completed in 1121 ms
说明:
- 这些
xxxAsync
函数不是 挂起 函数。它们可以在任何地方使用。 - 然而,它们总是在调用它们的代码中意味着异步(这里的意思是 并发 )执行
e)【使用async的结构化并发】
suspend fun concurrentSum(): Int = coroutineScope {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
one.await() + two.await()
}
输出:
The answer is 42
Completed in 1041 ms
取消始终通过协程的层次结构来进行传递:
import kotlinx.coroutines.*
fun main() = runBlocking<Unit> {
try {
failedConcurrentSum()
} catch(e: ArithmeticException) {
println("Computation failed with ArithmeticException")
}
}
suspend fun failedConcurrentSum(): Int = coroutineScope {
val one = async<Int> {
try {
delay(Long.MAX_VALUE) // 模拟一个长时间的运算
42
} finally {
println("First child was cancelled")
}
}
val two = async<Int> {
println("Second child throws an exception")
throw ArithmeticException()
}
one.await() + two.await()
}
l输出:
Second child throws an exception
First child was cancelled
Computation failed with ArithmeticException
说明:
- 如果其中一个子协程(即
two
)失败,第一个async
以及等待中的父协程都会被取消:
三、协程有什么优点
【协程很轻量】
import kotlinx.coroutines.*
fun main() = runBlocking {
repeat(100_000) { // 启动大量的协程
launch {
delay(1000L)
print(".")
}
}
}
它启动了10万个协程,并且在一秒钟之后,每个协程都输出一个点。如果是用线程来实现,将是一笔很大的开销。