1. 协程的出现
协程最早诞生于 1958 年,被应用于汇编语言中(距今已有 60 多年了),对它的完整定义发表于 1963 年,协程是一种通过代码执行的恢复与暂停来实现协作式的多任务的程序组件。而与此同时,线程的出现则要晚一些,伴随着操作系统的出现,线程大概在 1967 年被提出。
2. JAVA 对协程的支持
OpenJDK – Loom:其中引入了轻量级和高效的虚拟线程,而且是有栈协程
- JDK 引入一个新类:java.lang.Fiber。此类与 java.lang.Thread 一起,都成为了 java.lang.Strand 的子类
3. Kotlin 协程
在 JVM 平台上,Kotlin 协程是无栈协程,所谓无栈协程,是指协程在 suspend 状态时不需要保存调用栈。
入门
Hello World
fun main() {
GlobalScope.launch {
delay(1000)
println("Kotlin Coroutine: ${Thread.currentThread().name}")
}
println("Hello: ${Thread.currentThread().name}")
Thread.sleep(2000)
println("World: ${Thread.currentThread().name}")
}
执行结果:
Hello: main
Kotlin Coroutine: DefaultDispatcher-worker-1
World: main
代码解释:
如果不调用 Thread.sleep,那么 launch 里的代码不会执行,程序就退出了
至于为什么,先不做解释,读到后面自然就找到答案了
runBlocking
fun main() = runBlocking {
GlobalScope.launch {
delay(1000)
println("Kotlin Coroutines")
}
println("Hello")
delay(2000)
println("World")
}
执行结果:
Hello
Kotlin Coroutines
World
runBlocking 的官方 doc:
运行新的协程并中断阻塞当前线程,直到其完成
不应从协程使用此函数
桥接协程代码和非协程代码,通常用在 main 函数和单元测试
runBlocking 的思考
fun main() = runBlocking {
GlobalScope.launch {
delay(2000)
println("Kotlin Coroutines")
}
println("Hello")
delay(1000)
println("World")
}
执行结果:
Hello
World
原因分析:
GlobalScope 的 launch 函数返回一个 Job 对象
先来阅读一下 Job 的官方 doc
Job
核心作业接口 – 后台作业
-
后台作业是一个可以取消的东西,其生命周期最终以 completion 为终点
-
后台作业可以被安排到父子层次结构中,其中取消父级会导致立即递归取消其 children 所有作业。
-
另外,产生异常的子协程的失败(CancellationException 除外),将立即取消其父协程,从而取消其所有其他子协程。
-
Job 的最基本的实例接口是这样创建的:
- Coroutine job 是通过 CoroutineScope 的 launch 构建器创建的,它运行指定的代码块,并在完成此块时完成
- CompletableJob 是使用 Job()工厂函数创建的。它通过调用 CompletableJob.complete 来完成。
-
Job states
- 通常,Job 是在 Active state 下创建的。
- 然而,如果协程构建器提供了可选的启动参数会使协程在新的状态下,当使用 CoroutineStart.LAZY 的时候。此时可以通过调用 start 或者 join 来激活(active) Job
- 当协程处于工作状态时,Job 是激活状态的,直到其 Completed 或者它失败或者取消
- CompletableJob.complete 会将 Job 转换为 Completing 状态
- Completing 状态是 Job 的内部状态,对于外部观察者来说仍然是 Active 状态,而在内部,他正在等待他的子项
-
Job 接口及其所有派生接口对于第三方库中的继承并不稳定,因为将来可能会向此接口添加新方法,但可以使用稳定。
协程作用域 - GlobalScope
- 未绑定到任何 Job 的全局协程作用域
- 全局作用域用于启动顶级协程,这些协程在整个应用程序生命周期内运行,并且不会过早取消
- 它启动的协程不会使进程保持活动状态,类似守护线程
- 这个 API 被声明为微妙的,因为使用时很容易意外创建资源或内存泄漏
- 在有限的情况下,可以合法且安全地使用它,例如必须在应用程序的整个生命期内保持活跃的任务
- 那么如何解决上述不执行的问题,本质上是因为 runBlocking 和 GlobalScope.launch 是启动了两个独立的协程作用域,可以通过 Job.join 将两个作用域关联
// runBlocking 作用域
fun main() = runBlocking {
// GlobalScope.launch 作用域
val job: Job = GlobalScope.launch {
delay(1000)
println("Kotlin Coroutines")
}
println("Hello")
// 同一作用域下, 所有启动的协程全部完成后才会完成
// 但是, 此处是不同的作用域, 所以一定要 join
job.join()
println("World")
}
执行结果:
Hello
Kotlin Coroutines
World
- 每一个协程构建器都会向其代码块作用域中添加一个 CoroutineScope 实例
- 在同一个作用域下无需使用 join 函数,直接使用 launch 函数即可
fun main() = runBlocking {
launch {
delay(1000)
println("Kotlin Coroutines")
}
println("Hello")
}
执行结果:
Hello
Kotlin Coroutines
coroutineScope 函数
- 创建一个协程作用域,并且调用具有此作用域的挂起代码块
- 此函数设计用于并行分解工作。当此作用域中的任何子协程失败时,此作用域将失败,所有其他子项都将被取消
- 一旦给定块及其所有子协程完成,此函数就会返回
fun main() = runBlocking {
println(Thread.currentThread().name)
launch {
delay(1000)
println("my job1 -> ${Thread.currentThread().name}")
}
println("person -> ${Thread.currentThread().name}")
coroutineScope {
launch {
delay(10 * 1000)
println("my job2 -> ${Thread.currentThread().name}")
}
delay(5 * 1000)
println("hello world -> ${Thread.currentThread().name}")
}
launch {
println("block? -> ${Thread.currentThread().name}")
}
println("welcome -> ${Thread.currentThread().name}")
}
执行结果:
main
person -> main
my job1 -> main
hello world -> main
my job2 -> main
welcome -> main
block? -> main
协程的取消
fun main() = runBlocking {
val job = GlobalScope.launch {
repeat(200) {
println("hello: $it")
delay(500)
}
}
delay(1100)
println("Hello World")
// job.cancel()
// job.join()
job.cancelAndJoin()
println("welcome")
}
通过 cancelAndJoin 是 cancel 和 join 两个函数的结合
执行 job.cancelAndJoin(),执行结果为:
hello: 0
hello: 1
hello: 2
Hello World
welcome
执行 job.cancel(), 执行结果为:
hello: 0
hello: 1
hello: 2
Hello World
welcome
#执行结果和上述一致,虽然是不同的作用域
执行 job.join(), 执行结果为:
hello: 0
hello: 1
hello: 2
Hello World
hello: 3
hello: 4
hello: 5
···
hello: 199
- 协程在执行挂起函数前会检查当前是否是取消状态,如果是,则抛出 CancellationException,例外:如果协程正在处于某个计算过程中,并且没有检查取消状态,那么他是无法被取消的
- CancellationException
- 如果协程的作业在挂起时被取消,则由可取消的挂起函数抛出
- 它表示协程的正常取消
- 默认情况下,它不会打印到控制台日志中未捕获的异常处理程序
- CoroutineExceptionHandler:暂不展开
对于上述例外情况的举例
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 20) { // 此处就是处于计算过程中,而且没有检查取消状态,无法取消
if (System.currentTimeMillis() >= nextPrintTime) {
println("job: I am sleeping ${i++}")
nextPrintTime += 500L
}
}
}
delay(1300)
println("hello wworld")
job.cancelAndJoin()
println("welcome")
}
执行结果:
job: I am sleeping 0
job: I am sleeping 1
job: I am sleeping 2
hello wworld
job: I am sleeping 3
···
job: I am sleeping 19
welcome
- 有两种方式可以解决上述问题
- 周期性的调用一个挂起函数,该挂起函数会检查取消状态,比如使用 yield 函数
- 显示的检查取消状态
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
/*while (i < 20) { // 方式1
if (System.currentTimeMillis() >= nextPrintTime) {
println("job: I am sleeping ${i++}")
nextPrintTime += 500L
}
yield()
}*/
while (isActive) { // 方式2
if (System.currentTimeMillis() >= nextPrintTime) {
println("job: I am sleeping ${i++}")
nextPrintTime += 500L
}
}
}
delay(1300)
println("hello wworld")
job.cancelAndJoin()
println("welcome")
}
以上两种方式的执行结果:
job: I am sleeping 0
job: I am sleeping 1
job: I am sleeping 2
hello wworld
welcome
使用 finally 来关闭资源
- 当一个 job 没有执行完,调用了 cancelAndJoin,通常情况下需要有清理动作
- 另外会抛出 CancellationException
举例:
fun main() = runBlocking {
val job = launch {
try {
repeat(100) {
println("job repeat $it")
delay(500)
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
println("execute finally")
}
}
delay(1300)
println("hello")
job.cancelAndJoin()
println("world")
}
输出结果:
job repeat 0
job repeat 1
job repeat 2
hello
execute finally
world
kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@4f8e5cde
- 取消协程后,如果依旧调用挂起函数,会抛出异常
- 抛出 CancellationException
- 避免这种情况,可使用 withContext 方法,指定协程上下文
fun main() {
test()
//testNonCancellable()
}
fun test() = runBlocking {
val job = launch {
try {
repeat(100) {
println("job repeat $it")
delay(500)
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
println("execute finally")
delay(1000)
println("after delay 1000")
}
}
delay(1300)
println("hello")
job.cancelAndJoin()
println("world")
}
执行结果
job repeat 0
job repeat 1
job repeat 2
hello
execute finally
world
kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@4f8e5cde
fun testNonCancellable() = runBlocking {
val job = launch {
try {
repeat(100) {
println("job repeat $it")
delay(500)
}
} finally {
withContext(NonCancellable) {
println("execute finally")
delay(1000)
println("after delay 1000")
}
}
}
delay(1300)
println("hello")
job.cancelAndJoin()
println("world")
}
执行结果
job repeat 0
job repeat 1
job repeat 2
hello
execute finally
after delay 1000
world
- withTimeout 函数:kotlinx.coroutines.TimeoutCancellationException
- withTimeoutOrNull 函数:返回 null,不抛出异常
挂起函数
函数组合
- 挂起函数可以像普通函数一样在协程中
- 挂起函数可以使用其他的挂起函数
- 挂起函数只能用在协程中或是另一个挂起函数中
fun main() = runBlocking {
val elapsedTime = measureTimeMillis {
val value1 = intValue1()
val value2 = intValue2()
println("$value1 + $value2 = ${value1 + value2}")
}
println("total time: $elapsedTime")
}
private suspend fun intValue1(): Int {
delay(2000)
return 15
}
private suspend fun intValue2(): Int {
delay(3000)
return 20
}
输出结果:
15 + 20 = 35
total time: 5010
async 和 await
- 通过这两个函数可以实现并发
- 从概念上讲,async 和 launch 一样,他会开启一个单独的协程
- 区别是,launch 返回的是一个 Job,Job 并不会持有任何结果值;async 会返回一个 Deferred,类似 future 和 promise,持有一个结果值
- 通过在 Deferred 上调用 await 方法获取最终的结果值
- Deferred 是 Job 的子类,是非阻塞的,可取消的 future
fun main() = runBlocking {
val elapsedTime = measureTimeMillis {
val s1 = async { intValue1() }
val s2 = async { intValue2() }
val value1 = s1.await()
val value2 = s2.await()
println("$value1 + $value2 = ${value1 + value2}")
}
println("total time: $elapsedTime")
}
private suspend fun intValue1(): Int {
delay(2000)
return 15
}
private suspend fun intValue2(): Int {
delay(3000)
return 20
}
执行结果:
15 + 20 = 35
total time: 3018
和 Job 一样,Deferred 如果启动参数设置为 CoroutineStart.LAZY,那么同样需要先激活,Deferred 比 Job 多了一个可以激活状态的方法:await
fun main() = runBlocking {
val elapsedTime = measureTimeMillis {
val s1 = async(start = CoroutineStart.LAZY) {
intValue1()
}
val s2 = async(start = CoroutineStart.LAZY) {
intValue2()
}
println("hello world")
Thread.sleep(2500)
delay(2500)
val value1 = s1.await()
val value2 = s2.await()
println(value1 + value2)
}
println("total time: $elapsedTime")
}
private suspend fun intValue1(): Int {
delay(2000)
return 15
}
private suspend fun intValue2(): Int {
delay(3000)
return 20
}
执行结果
hello world
35
total time: 10032
结构化并发程序开发
fun main() = runBlocking {
val elapsedTime = measureTimeMillis {
println("intSum: ${intSum()}")
}
println("total time: $elapsedTime")
}
private suspend fun intSum(): Int = coroutineScope<Int> {
val s1 = async { intValue1() }
val s2 = async { intValue2() }
s1.await() + s2.await()
}
private suspend fun intValue1(): Int {
delay(2000)
return 15
}
private suspend fun intValue2(): Int {
delay(3000)
return 20
}
执行结果
intSum: 35
total time: 3016
协程上下文
分发器
- 协程总是会在某个上下文中执行,这个上下文是由 CoroutineContext 类型的实例来表示的
- CoroutineContext 的继承关系如下
- 协程上下文本质上是各种元素所构成的一个集合,主要元素包括 Job 以及 CoroutineDispatcher
- CoroutineDispatcher 的主要功能是确定协程由哪个线程来执行所指定的代码,可以限制到一个具体的线程,也可以分发到一个线程池中,还可以不加任何限制(这种情况下代码执行的线程是不确定的,开发中不建议使用)
- 所有的协程构建器(如 launch 和 async)的方法可选参数中可以指定一个 CoroutineContext
fun main() = runBlocking<Unit> {
launch {
println("no param, thread: ${Thread.currentThread().name}")
}
launch(Dispatchers.Unconfined) {
println("dispatchers unconfined, thread: ${Thread.currentThread().name}")
delay(100) // 加上延迟,就会发现不是运行在main线程了
println("dispatchers unconfined, thread: ${Thread.currentThread().name}")
}
launch(Dispatchers.Default) {
println("dispachers default, thread: ${Thread.currentThread().name}")
}
val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
launch(dispatcher) {
println("single thread executor service, thread: ${Thread.currentThread().name}")
dispatcher.close()
}
GlobalScope.launch {
println("globle scope launch, thread: ${Thread.currentThread().name}")
}
}
执行结果:
dispatchers unconfined, thread: main
dispachers default, thread: DefaultDispatcher-worker-1
single thread executor service, thread: pool-1-thread-1
globle scope launch, thread: DefaultDispatcher-worker-1
no param, thread: main
dispatchers unconfined, thread: kotlinx.coroutines.DefaultExecutor
- 当通过 launch 来启动协程且不指定协程分发器时,它会继承启动它的那个 CoroutineScope 的上下文与分发器,对该示例来说,它会继承 runBlocking 的上下文,而 runBlocking 则是运行在 main 线程当中
- Dispatchers.Unconfined 是一种很特殊的协程分发器,它在该示例中一开始是 main 线程,但是后来线程发生变化
- Dispatchers.Default 是默认的分发器,当协程是通多 GlobalScope 来启动的时候,它会使用该默认的分发器来启动协程,它会使用一个后台的共享线程池来运行我们的协程代码。因此,launch(Dispatchers.Default) 等价于(这里只说线程, 作用域是不同的) GlobalScope.launch { }
- asCoroutineDispatcher Kotlin 提供的扩展方法,使得线程池来执行我们所指定的协程代码。在实际开法中,使用专门的线程池来执行协程代码代价是非常高的,因此在协程代码执行完毕后,我们必须要释放相应的资源,这里就需要使用 close 方法来关闭相应的协程分发器,从而释放资源;也可以将该协程分发器存储到一个顶层变量中,以便在程序的其他地方进行复用
- asCoroutineDispatcher 如果指定的线程池是 ScheduledExecutorService,那么 delay、withTimeout、Flow 等所有时间相关的操作会在此线程池上计算;如果指定的线程池是 ScheduledThreadPoolExecutor,那么还会设置 setRemoveOnCancelPolicy 来减小内存压力;如果指定的线程池不是上述类型,时间相关的操作将在其他线程计算,但协程本身仍将在给定的执行器之上执行;如果指定线程池引发 RejectedExecutionException,则会取消受影响的 Job,并提交到 Dispatchers.IO 以便受影响的协程可以清理其资源并迅速完成
fun main() = runBlocking<Unit> {
val singleDispatcher = ThreadPoolExecutor(
1,
1,
0,
TimeUnit.SECONDS,
ArrayBlockingQueue(1)
).asCoroutineDispatcher()
repeat(10) {
launch(singleDispatcher) {
try {
delay(100)
println("${Thread.currentThread().name} - $it")
} catch (e: Exception) {
println(e.message)
} finally {
println("${Thread.currentThread().name} - finally")
}
}
}
}
执行结果:
pool-1-thread-1 - 0
pool-1-thread-1 - finally
The task was rejected
DefaultDispatcher-worker-1 - finally
pool-1-thread-1 - 3
pool-1-thread-1 - finally
The task was rejected
DefaultDispatcher-worker-1 - finally
pool-1-thread-1 - 6
pool-1-thread-1 - finally
The task was rejected
DefaultDispatcher-worker-1 - finally
pool-1-thread-1 - 9
pool-1-thread-1 - finally
调试
- Kotlin 协程可开启 debug 参数,通过 JVM option 指定:-Dkotlinx.coroutines.debug
- 我们可以发现,打印线程名的时候,也会把协程名称打印出来
private fun log(msg: String) {
println("[${Thread.currentThread().name}] - $msg")
}
fun main() = runBlocking {
val s1 = async {
delay(2000)
log("hello world")
10
}
val s2 = async {
log("welcome")
20
}
log("The result is ${s1.await() + s2.await()}")
}
执行结果:
[main @coroutine#3] - welcome
[main @coroutine#2] - hello world
[main @coroutine#1] - The result is 30
/////////////////////////////////////////////////////
private fun log(msg: String) {
println("[${Thread.currentThread().name}] - $msg")
}
fun main() {
newSingleThreadContext("Context1").use { ctx1 ->
newSingleThreadContext("Context2").use { ctx2 ->
runBlocking(ctx1) {
log("started in context1")
withContext(ctx2) {
log("working in context2")
}
log("Back to context1")
}
}
}
}
执行结果:
[Context1 @coroutine#1] - started in context1
[Context2 @coroutine#1] - working in context2
[Context1 @coroutine#1] - Back to context1
通过查看 CoroutineContext 的继承关系和源码得知,每一个 Element 都有一个名为 Key 的半生对象
fun main() = runBlocking {
val job: Job? = coroutineContext[Job]
println(job)
println(coroutineContext.isActive)
}
执行结果
BlockingCoroutine{Active}@4459eb14
true
ThreadLocal
asContextElement 扩展方法可以转换成 CoroutineContext 对象
val threadLocal = ThreadLocal<String>()
private fun printCurrentThreadValue(info: String) {
println("$info, ${Thread.currentThread().name}, local value is: ${threadLocal.get()}")
}
fun main() = runBlocking {
val newDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
threadLocal.set("Main Value")
printCurrentThreadValue("start main")
val job = launch(Dispatchers.Default + threadLocal.asContextElement("Coroutine Value")) {
printCurrentThreadValue("launch a coroutine")
yield()
printCurrentThreadValue("coroutine after yield")
launch {
printCurrentThreadValue("run in child")
}
launch(newDispatcher) {
printCurrentThreadValue("run in new thread")
}
}
val job2 = launch(Dispatchers.Default) {
printCurrentThreadValue("run in other coroutine")
}
val job3 = launch {
printCurrentThreadValue("run in other2 coroutine")
}
job.join()
job2.join()
job3.join()
newDispatcher.close()
printCurrentThreadValue("end main")
}
执行结果:
start main, main, local value is: Main Value
launch a coroutine, DefaultDispatcher-worker-1, local value is: Coroutine Value
coroutine after yield, DefaultDispatcher-worker-1, local value is: Coroutine Value
run in child, DefaultDispatcher-worker-2, local value is: Coroutine Value
run in other coroutine, DefaultDispatcher-worker-2, local value is: null
run in new thread, pool-1-thread-1, local value is: Coroutine Value
run in other2 coroutine, main, local value is: Main Value
end main, main, local value is: Main Value
可以发现 ThreadLocal 通过使用 asContextElement 方法,协程之间共享数据遵循协程作用域的范围。
5. 参考资料
https://github.com/JetBrains/kotlin
6. 团队介绍
「三翼鸟数字化技术平台-智家APP平台」 通过持续迭代演进移动端一站式接入平台为三翼鸟APP、智家APP等多个APP提供基础运行框架、系统通用能力API、日志、网络访问、页面路由、动态化框架、UI组件库等移动端开发通用基础设施;通过Z·ONE平台为三翼鸟子领域提供项目管理和技术实践支撑能力,完成从代码托管、CI/CD系统、业务发布、线上实时监控等Devops与工程效能基础设施搭建。