Coroutine是kotlin官方文档上推荐的,个人理解,其实就是一个轻量级的线程库。当然,协程并不是线程.简单来说,线程(thread)的调度是由操作系统负责,线程的睡眠、等待、唤醒的时机是由操作系统控制,开发者无法决定。使用协程,开发者可以自行控制切换的时机,可以在一个函数执行到一半的时候中断执行,让出CPU,在需要的时候再回到中断点继续执行。因为切换的时机是由开发者来决定的,就可以结合业务的需求来实现一些高级的特性。
使用前加依赖
代码语言:javascript复制implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2")
一、基础
我们可以通过如下代码开启一个协程
代码语言:javascript复制import kotlinx.coroutines.*
fun main() {
GlobalScope.launch { // 在后台启动一个新的协程并继续
delay(1000L) // 非阻塞的等待 1 秒钟(默认时间单位是毫秒)
println("World!") // 在延迟后打印输出
}
println("Hello,") // 协程已在等待时主线程还在继续
Thread.sleep(2000L) // 阻塞主线程 2 秒钟来保证 JVM 存活
// runBlocking { // 但是这个表达式阻塞了主线程
// delay(2000L) // ……我们延迟 2 秒来保证 JVM 的存活
// }
}
代码运行结果就是
代码语言:javascript复制Hello,
World!
其实GlobalScope.launch可以通过Thread来替代,但要注意delay是一个特殊的 挂起函数 ,它不会造成线程阻塞,但是会 挂起 协程,并且只能在协程中使用。 当然,也可以用后面的 runBlocking 。调用了 runBlocking 的主线程会一直 阻塞 直到 runBlocking 内部的协程执行完毕。 当然,可以使用更合乎惯用法的方式重写,使用 runBlocking 来包装 main 函数的执行:
代码语言:javascript复制fun main() = runBlocking<Unit> { // 开始执行主协程
GlobalScope.launch { // 在后台启动一个新的协程并继续
delay(1000L)
println("World!")
}
println("Hello,") // 主协程在这里会立即执行
delay(2000L) // 延迟 2 秒来保证 JVM 存活
}
延迟一段时间来等待另一个协程运行并不是一个好的选择。让我们显式(以非阻塞方式)等待所启动的后台Job执行结束:
代码语言:javascript复制import kotlinx.coroutines.*
fun main() = runBlocking {
val job = GlobalScope.launch { // 启动一个新协程并保持对这个作业的引用
delay(1000L)
println("World!")
}
println("Hello,")
job.join() // 等待直到子协程执行结束
}
现在,结果仍然相同,但是主协程与后台作业的持续时间没有任何关系
当我们使用 GlobalScope.launch
时,我们会创建一个顶层协程。虽然它很轻量,但它运行时仍会消耗一些内存资源。如果我们忘记保持对新启动的协程的引用,它还会继续运行。如果协程中的代码挂起了会怎么样(例如,我们错误地延迟了太长时间),如果我们启动了太多的协程并导致内存不足会怎么样? 这个时候我们可以让他们在同一个协程里处理
import kotlinx.coroutines.*
fun main() = runBlocking { // this: CoroutineScope
launch { // 在 runBlocking 作用域中启动一个新协程
delay(1000L)
println("World!")
}
println("Hello,")
}
我们来将 launch { …… } 内部的代码块提取到独立的函数中。当你对这段代码执行“提取函数”重构时,你会得到一个带有 suspend 修饰符的新函数。 这是你的第一个挂起函数。在协程内部可以像普通函数一样使用挂起函数, 不过其额外特性是,同样可以使用其他挂起函数(如本例中的 delay)来挂起协程的执行。
代码语言:javascript复制import kotlinx.coroutines.*
fun main() = runBlocking {
launch { doWorld() }
println("Hello,")
}
// 这是你的第一个挂起函数
suspend fun doWorld() {
delay(1000L)
println("World!")
}
协程很轻量
代码语言:javascript复制import kotlinx.coroutines.*
fun main() = runBlocking {
repeat(100000) { // 启动大量的协程
launch {
delay(5000L)
print(".")
}
}
}
同样的方法用线程来执行可能会报内存不足 全局协程像守护线程 在 GlobalScope中启动的活动协程并不会使进程保活。它们就像守护线程。
代码语言:javascript复制import kotlinx.coroutines.*
fun main() = runBlocking {
GlobalScope.launch {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
delay(1300L) // 在延迟后退出
}
上面的代码只会输出三行就结束了
代码语言:javascript复制I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
当改成launch以后会全部输出,这个时候最后的delay也就没用了
二、取消与超时
可以通过job.cancel来取消协程
代码语言:javascript复制import kotlinx.coroutines.*
fun main() = runBlocking {
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.")
}
输出如下
代码语言:javascript复制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.
很明显,在cancel之后就不会打印 job: I'm sleeping 了。现在看下面的代码
代码语言:javascript复制import kotlinx.coroutines.*
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 5) { // 一个执行计算的循环,只是为了占用 CPU
// 每秒打印消息两次
if (System.currentTimeMillis() >= nextPrintTime) {
println("job: I'm sleeping ${i } ...")
nextPrintTime = 500L
}
}
}
delay(1300L) // 等待一段时间
println("main: I'm tired of waiting!")
job.cancelAndJoin() // 取消一个作业并且等待它结束
println("main: Now I can quit.")
}
在cancel之后仍然打印 job: I'm sleeping 。为啥呢?因为一段协程代码必须协作才能被取消
协程的取消是 协作 的。一段协程代码必须协作才能被取消。 所有 kotlinx.coroutines
中的 挂起函数 都是 可被取消的 。它们检查协程的取消, 并在取消时抛出 CancellationException。 然而,如果协程正在执行计算任务,并且没有检查取消的话,那么它是不能被取消的
我们有两种方法来使执行计算的代码可以被取消. 第一种方法是定期调用挂起函数来检查取消。delay,yield是一个好的选择。 第二种方法是显式的检查取消状态。
所以,打印没结束就是因为launch内部没有挂起函数。比如delay就是挂起函数.可以在println前面加上delay(1L)就可以让循环停下来。或者用 while (i < 5) 替换为 while (isActive)。isActive 是一个可以被使用在 CoroutineScope中的扩展属性。
另外,我们可以在 finally
中释放资源
fun main() = runBlocking {
val job = launch {
try {
repeat(1000) { i ->
println("job: I'm sleeping $i ...")
delay(500L)
}
} finally {
println("job: I'm running finally")
}
}
delay(1300L) // 延迟一段时间
println("main: I'm tired of waiting!")
job.cancelAndJoin() // 取消该作业并且等待它结束
println("main: Now I can quit.")
}
但是注意,finally
中不能有挂起函数。任何尝试在 finally
块中调用挂起函数的行为都会抛出 CancellationException。当你需要挂起一个被取消的协程,你可以将相应的代码包装在 withContext(NonCancellable) {……}
中,如下所示:
fun main() = runBlocking {
val job = launch {
try {
repeat(1000) { i ->
println("job: I'm sleeping $i ...")
delay(500L)
}
} finally {
withContext(NonCancellable) {
println("job: I'm running finally")
delay(1000L)
println("job: And I've just delayed for 1 sec because I'm non-cancellable")
}
}
}
delay(1300L) // 延迟一段时间
println("main: I'm tired of waiting!")
job.cancelAndJoin() // 取消该作业并等待它结束
println("main: Now I can quit.")
}
输出如下
代码语言:javascript复制job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
job: I'm running finally
job: And I've just delayed for 1 sec because I'm non-cancellable
main: Now I can quit.
现在一个很重要的问题就是 CancellationException 好像并没有在控制台显式的展示出来。下面将展示一个显式展示的例子
代码语言:javascript复制fun main() = runBlocking {
withTimeout(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
}
输出如下
代码语言:javascript复制I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms
withTimeout 抛出了 TimeoutCancellationException
,它是 CancellationException 的子类。 我们之前没有在控制台上看到堆栈跟踪信息的打印。这是因为在被取消的协程中 CancellationException
被认为是协程执行结束的正常原因。 然而,在这个示例中我们在 main
函数中正确地使用了 withTimeout
。
由于取消只是一个例外,所有的资源都使用常用的方法来关闭。 如果你需要做一些各类使用超时的特别的额外操作,可以使用类似 withTimeout 的 withTimeoutOrNull 函数,并把这些会超时的代码包装在 try {...} catch (e: TimeoutCancellationException) {...}
代码块中,而 withTimeoutOrNull 通过返回 null
来进行超时操作,从而替代抛出一个异常
fun main() = runBlocking {
val result = withTimeoutOrNull(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
println("Result is $result")//这里result是null
}
三、组合挂起函数
代码语言:javascript复制import kotlinx.coroutines.*
import kotlin.system.*
fun main() = runBlocking<Unit> {
val time = measureTimeMillis {
val one = doSomethingUsefulOne()
val two = doSomethingUsefulTwo()
println("The answer is ${one two}")
}
println("Completed in $time ms")
}
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // 假设我们在这里做了些有用的事
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // 假设我们在这里也做了一些有用的事
return 29
}
这段代码输出
代码语言:javascript复制The answer is 42
Completed in 2008 ms
如果 doSomethingUsefulOne
与 doSomethingUsefulTwo
之间没有依赖,并且我们想更快的得到结果,让它们进行 并发 吗?我们可以使用 async 可以帮助我们的地方。
async
返回一个 Deferred —— 一个轻量级的非阻塞 future, 这代表了一个将会在稍后提供结果的 promise。你可以使用 .await()
在一个延期的值上得到它的最终结果, 但是 Deferred
也是一个 Job
,所以如果需要的话,你可以取消它。(注意,使用协程进行并发总是显式的)
import kotlinx.coroutines.*
import kotlin.system.*
fun main() = runBlocking<Unit> {
val time = measureTimeMillis {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
println("The answer is ${one.await() two.await()}")
}
println("Completed in $time ms")
}
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // 假设我们在这里做了些有用的事
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // 假设我们在这里也做了些有用的事
return 29
}
输出如下
代码语言:javascript复制The answer is 42
Completed in 1018 ms
当然也可以选择惰性启动。将 start
参数设置为 CoroutineStart.LAZY] 而变为惰性的。 在这个模式下,只有结果通过 await 获取的时候协程才会启动,或者在 Job
的 start 函数调用的时候。
import kotlinx.coroutines.*
import kotlin.system.*
fun main() = runBlocking<Unit> {
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")
}
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // 假设我们在这里做了些有用的事
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // 假设我们在这里也做了些有用的事
return 29
}
输出其实是一样的
四、协程上下文与调度器
协程调度器可以将协程限制在一个特定的线程执行,或将它分派到一个线程池,亦或是让它不受限地运行。
- 当调用
launch { …… }
时不传参数,它承袭了当前协程的上下文(以及调度器)。 - Dispatchers.Unconfined 是一个特殊的调度器且似乎也运行在
main
线程中,但实际上, 它是一种不同的机制,这会在后文中讲到。 - 当协程在 GlobalScope 中启动时,使用的是由 Dispatchers.Default 代表的默认调度器。 默认调度器使用共享的后台线程池。 所以
launch(Dispatchers.Default) { …… }
与GlobalScope.launch { …… }
使用相同的调度器。 - newSingleThreadContext 为协程的运行启动了一个线程。 一个专用的线程是一种非常昂贵的资源。 在真实的应用程序中两者都必须被释放,当不再需要的时候,使用 close 函数,或存储在一个顶层变量中使它在整个应用程序中被重用。
在Android中的用法如下
代码语言:javascript复制class Activity {
//MainScope其实是专门为UI创建,默认Dispatchers.Main
private val mainScope = MainScope()
fun destroy() {
mainScope.cancel()
}
fun doSomething() {
// 在示例中启动了 10 个协程,且每个都工作了不同的时长
repeat(10) { i ->
mainScope.launch {
delay((i 1) * 200L) // 延迟 200 毫秒、400 毫秒、600 毫秒等等不同的时间
println("Coroutine $i is done")
}
}
}
}