参考资料

  • 协程的所在核心的Github:

Kotlin/kotlinx.coroutines

  • 协程的官方文档

Kotlin/kotlinx.coroutines

Kotlin Coroutines(协程) 完全解析(二),深入理解协程的挂起、恢复与调度

如何正确的在Android上使用协程?_秉心说-CSDN博客

环境配置

截至2019/5/20 22:46 Gradle配置如下:

Java:(IDEA)

  1. dependencies {
  2. implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
  3. implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1'
  4. }

Andriod:

  1. implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1'
  2. implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1'

基本应用场景

协程最基本的用法

  1. val job = GlobalScope.launch{
  2. }
  3. //可以在需要时取消
  4. job.cancel();

注:启动一个新的协程,而不会阻塞当前线程,返回一个协程Job,Job可用来控制对应协程序,比如cancel

如何让协程运行在主线程上

  • 方案一:
  1. GlobalScope.launch(Dispatchers.Main){
  2. println("执行协程")
  3. }
  • 方案二
  1. MainScope().launch {
  2. println("执行协程")
  3. }

协程的线程池调度策略(协程的本质是线程池的调度)

如上例,我们可以用Dispatchers.Main使协程运行到主线程,不同的Dispatchers定义了不同的协程调度模式

  1. GlobalScope.launch(Dispatchers.Main){
  2. println("在主线程执行协程")
  3. }
策略 描述
Dispatchers.Main 运行在主线程/UI线程上
Dispatchers.Unconfined 运行在当前所在线程
Dispatchers.Default 默认值的CoroutineDispatcher,JVM共享线程池,保证最大程度的并行性,线程数量等于CPU的核心数,最少为2
Dispatchers.IO IO线程池,它默认为64个线程的极限或核的数量的最大值

协程中的堵塞:sleep及dalay

  • 一般情况下,非常不建议在协程中用 Thread.sleep()来实现堵塞(会直接堵塞线程),建议用 delay()来实现协程的堵塞(可以被取消且仅挂起协程而不堵塞线程)
  • 默认情况下,协程中的Thread.sleep()并不影响所在线程后续代码的执行,这种不影响当前所在线程的状态,称之为挂起,如下:
  1. fun testBlock(){
  2. println("${LocalDateTime.now()}:开始测试")
  3. GlobalScope.launch {
  4. println("${LocalDateTime.now()}:协程开始运行")
  5. Thread.sleep(2000)
  6. println("${LocalDateTime.now()}:协程结束运行")
  7. }
  8. Thread.sleep(500)//确保协程已经被启动
  9. println("${LocalDateTime.now()}:结束测试")
  10. }

System.out: 2022-10-19T03:04:03.509:开始测试

System.out: 2022-10-19T03:04:03.532:协程开始运行

System.out: 2022-10-19T03:04:04.031:结束测试

System.out: 2022-10-19T03:04:05.533:协程结束运行

注:这很好理解,因为协程的本质是线程池,把GlobalScope.launch看成Thread{ },就明白了。换一种说法,如果我们让协程运行的线程是当前所在线程,那么就会导致整体堵塞,无法挂起了,如下:

  1. fun testBlock(){
  2. println("${LocalDateTime.now()}:开始测试")
  3. //让协程运行在当前所在线程
  4. GlobalScope.launch(Dispatchers.Unconfined) {
  5. println("${LocalDateTime.now()}:协程开始运行")
  6. Thread.sleep(2000)
  7. println("${LocalDateTime.now()}:协程结束运行")
  8. }
  9. Thread.sleep(500)//确保协程已经被启动
  10. println("${LocalDateTime.now()}:结束测试")
  11. }

System.out: 2022-10-19T03:12:59.997:开始测试

System.out: 2022-10-19T03:13:00.019:协程开始运行

System.out: 2022-10-19T03:13:02.021:协程结束运行

System.out: 2022-10-19T03:13:02.525:结束测试

这段代码实际上相当于:

  1. fun testBlock(){
  2. println("${LocalDateTime.now()}:开始测试")
  3. println("${LocalDateTime.now()}:协程开始运行")
  4. Thread.sleep(2000)
  5. println("${LocalDateTime.now()}:协程结束运行")
  6. Thread.sleep(500)//确保协程已经被启动
  7. println("${LocalDateTime.now()}:结束测试")
  8. }

我们可以换成delay的方式来堵塞,会更符合我们的预期:挂起协程。

  1. fun testBlock(){
  2. println("${LocalDateTime.now()}:开始测试")
  3. //让协程运行在当前所在线程
  4. GlobalScope.launch(Dispatchers.Unconfined) {
  5. println("${LocalDateTime.now()}:协程开始运行")
  6. delay(2000)
  7. println("${LocalDateTime.now()}:协程结束运行")
  8. }
  9. Thread.sleep(500)//确保协程已经被启动
  10. println("${LocalDateTime.now()}:结束测试")
  11. }

System.out: 2022-10-19T03:50:11.157:开始测试

System.out: 2022-10-19T03:50:11.177:协程开始运行

System.out: 2022-10-19T03:50:11.684:结束测试

System.out: 2022-10-19T03:50:13.183:协程结束运行

协程的取消

情况一:

  1. fun testBlock(){
  2. val job = GlobalScope.launch{
  3. println("${LocalDateTime.now()}:协程开始运行")
  4. Thread.sleep(2000)
  5. println("${LocalDateTime.now()}:协程结束运行")
  6. }
  7. job.cancel()//如果job协程的执行先于cancel,则无法cancel
  8. }

情况二:

  1. fun testBlock(){
  2. val job = GlobalScope.launch{
  3. println("${LocalDateTime.now()}:协程开始运行")
  4. delay(2000)//堵塞2000毫秒
  5. println("${LocalDateTime.now()}:协程结束运行")
  6. }
  7. //在协程delay期间是可以被取消的,后续代码不再执行
  8. job.cancel()
  9. }

挂起函数

挂起函数的应用场景之一在于取代耗时操作后回调的函数,挂起函数的本质上也是回调。挂起函数只能在协程中被调用。

  1. public interface Callback{
  2. public fun onMsg(msg:String);
  3. }
  4. public fun queryMsg(callback:Callback){
  5. Thread{
  6. Thread.sleep(2000);
  7. callback.onMsg("msg")
  8. }.start()
  9. }

可以替换为:

  1. public suspend fun queryMsg():String{
  2. delay(2000);//模拟耗时操作
  3. return "msg";
  4. }

注:挂起函数必须加上suspend关键字,Kotlin 的编译器会将 suspend 函数转换为带有 Callback 的普通函数。