Kotlin中级——协程

协程

协程可以暂停执行,而不是阻塞线程。这允许一个协程在等待某些数据到达时挂起,另一个协例程在同一线程上运行,从而确保有效的资源利用率。

依赖

复制代码
dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0'
}

协程创建

创建协程,需要

  • suspend 方法
  • 协程作用域,如withContext()
  • 协程构造器,如CoroutineScope.launch()
  • 调度器,用于控制协程使用的线程,如Dispatchers.Default

如下,相当于挂起,切换到线程池,运行里面的协程代码,等全部运行完之后,再切换到原来的环境,期间主线程完全空闲,可以做别的任何事

kt 复制代码
class CoroutinesBasicsTest {

    suspend fun greet() {
        println("The greet() on the thread: ${Thread.currentThread().name}")
        // Suspends for 1 second and releases the thread
        delay(1.seconds)
        // The delay() function simulates a suspending API call here
        // You can add suspending API calls here like a network request
    }

    suspend fun main() {
        // Runs the code inside this block on a shared thread pool
        withContext(Dispatchers.Default) { // this: CoroutineScope
            this.launch() {
                greet()
            }

            // Starts another coroutine
            this.launch() {
                println("The CoroutineScope.launch() on the thread: ${Thread.currentThread().name}")
                delay(1.seconds)
                // The delay function simulates a suspending API call here
                // You can add suspending API calls here like a network request
            }

            println("The withContext() on the thread: ${Thread.currentThread().name}")
        }
    }

    @Test
    fun runCoroutinesExample() = runBlocking {
        println("------------------------------------------------------------------------------------------------------------------------------------------------------")
        main()
        println("------------------------------------------------------------------------------------------------------------------------------------------------------")
    }
}

打印类似如下,每次输出顺序和线程名称都不同,取决于闲置的线程

复制代码
The greet() on the thread: DefaultDispatcher-worker-2 @coroutine#2
The withContext() on the thread: DefaultDispatcher-worker-1 @coroutine#1
The CoroutineScope.launch() on the thread: DefaultDispatcher-worker-3 @coroutine#3

The greet() on the thread: DefaultDispatcher-worker-2 @coroutine#2
The CoroutineScope.launch() on the thread: DefaultDispatcher-worker-3 @coroutine#3
The withContext() on the thread: DefaultDispatcher-worker-1 @coroutine#1

Suspend

允许正在运行的操作在不影响代码结构的情况下暂停和稍后恢复,只能从另一个Suspend函数调用Suspend函数

协程作用域

  • 父协程在完成之前等待其子协程完成。如果父协程失败或被取消,则其所有子协程也会被递归取消
  • 新的协程只能在定义和管理其生命周期的CoroutionScope中启动
  • 当你在另一个协程中启动一个例程时,它会自动成为其父作用域的子作用域。如CoroutinScope.launch()启动的任何协程都会成为它的子协程

coroutineScope

如下,coroutineScope()开个新作用域,继承上下文调度器(未指定则为Dispatchers.Default),等里面所有协程全跑完才返回

  • coroutineScope() 继承的调度器等于Dispatchers.Default时,相当于withContext(Dispatchers.Default),withContext多了一个调度和环境切换功能
kt 复制代码
class CoroutinesBasicsTest {

    suspend fun main() {
        // Root of the coroutine subtree
        coroutineScope { // this: CoroutineScope
            this.launch {
                this.launch {
                    delay(2.seconds)
                    println("Child of the enclosing coroutine completed")
                }
                println("Child coroutine 1 completed")
            }
            this.launch {
                delay(1.seconds)
                println("Child coroutine 2 completed")
            }
        }
        // Runs only after all children in the coroutineScope have completed
        println("Coroutine scope completed")
    }

    @Test
    fun runCoroutinesExample() = runBlocking {
        println("------------------------------------------------------------------------------------------------------------------------------------------------------")
        main()
        println("------------------------------------------------------------------------------------------------------------------------------------------------------")
    }
}

协程构建器

CoroutineScope.launch

在现有协程作用域内启动一个新的协程,而不会阻塞作用域的其余部分。返回一个Job句柄,使用此句柄等待启动的协程完成

复制代码
class CoroutinesBasicsTest {

    suspend fun performBackgroundWork() = coroutineScope { // this: CoroutineScope
        // Starts a coroutine that runs without blocking the scope
        this.launch {
            // Suspends to simulate background work
            delay(100.milliseconds)
            println("Sending notification in background")
        }

        // Main coroutine continues while a previous one suspends
        println("Scope continues")
    }

    @Test
    fun runCoroutinesExample() = runBlocking {
        println("------------------------------------------------------------------------------------------------------------------------------------------------------")
        performBackgroundWork()
        println("------------------------------------------------------------------------------------------------------------------------------------------------------")
    }
}

Scope continues
Sending notification in background

CoroutineScope.async

在现有协程范围内启动并发计算,并返回一个表示最终结果的Deferred句柄。使用await()函数挂起代码,直到结果就绪

复制代码
class CoroutinesBasicsTest {

    suspend fun main() = withContext(Dispatchers.Default) { // this: CoroutineScope
        // Starts downloading the first page
        val firstPage = this.async {
            delay(50.milliseconds)
            "First page"
        }

        // Starts downloading the second page in parallel
        val secondPage = this.async {
            delay(100.milliseconds)
            "Second page"
        }

        // Awaits both results and compares them
        val pagesAreEqual = firstPage.await() == secondPage.await()
        println("Pages are equal: $pagesAreEqual")
    }

    @Test
    fun runCoroutinesExample() = runBlocking {
        println("------------------------------------------------------------------------------------------------------------------------------------------------------")
        main()
        println("------------------------------------------------------------------------------------------------------------------------------------------------------")
    }
}

Pages are equal: false

runBlocking

创建一个协程作用域,并阻塞当前线程,直到在该作用域中启动的协程完成

仅当没有其他选项从非挂起代码调用挂起代码时,才使用runBlocking,如java调用kt代码suspend,需要同步返回

复制代码
// A third-party interface you can't change
interface Repository {
    fun readItem(): Int
}

object MyRepository : Repository {
    override fun readItem(): Int {
        // Bridges to a suspending function
        return runBlocking {
            myReadItem()
        }
    }
}

suspend fun myReadItem(): Int {
    delay(100.milliseconds)
    return 4
}

协程调度器

控制哪个线程或线程池协程用于执行,协程并不总是与单个线程相关联。他们可以在一个线程上暂停,在另一个线程中继续

默认情况下,协程从其父作用域继承调度器,如果协程上下文不包括调度器,默认Dispatchers.Default

复制代码
suspend fun runWithDispatcher() = coroutineScope { // this: CoroutineScope
    this.launch(Dispatchers.Default) {
        println("Running on ${Thread.currentThread().name}")
    }
}

取消和超时

如下代码

  • 变量childStarted确保协程先启动再取消

  • awaitCancellation()让协程挂起,直到它被取消,相当于delay(Duration.INFINITE)

  • launch返回job句柄,调用cancel()

  • awaitCancellation()在下次检查取消时抛出CancellationException,捕获异常后一定要再次抛出

    class CoroutinesBasicsTest {

    复制代码
      @Test
      fun runCoroutinesExample() = runBlocking {
          println("------------------------------------------------------------------------------------------------------------------------------------------------------")
          withContext(Dispatchers.Default) {
              // Used as a signal that the coroutine has started running
              val childStarted = CompletableDeferred<Unit>()
    
              val childJob: Job = launch {
                  println("The coroutine has started")
    
                  // Completes the CompletableDeferred,
                  // signaling that the coroutine has started running
                  childStarted.complete(Unit)
                  try {
                      // Suspends indefinitely
                      // This call will never return unless the coroutine is canceled
                      awaitCancellation()
                  } catch (e: CancellationException) {
                      println("The coroutine was canceled: $e")
    
                      // Always rethrow cancellation exceptions!
                      throw e
                  }
                  println("This line will never be executed")
              }
    
              // Waits for the coroutine to start before canceling it
              childStarted.await()
    
              // Cancels the coroutine,
              // so awaitCancellation() throws a CancellationException
              childJob.cancel()
          }

    // Coroutine builders such as withContext() or coroutineScope()
    // wait for all child coroutines to complete,
    // even when the children are canceled
    println("All coroutines have completed")
    println("------------------------------------------------------------------------------------------------------------------------------------------------------")
    }
    }

取消传递

取消协程也会取消其所有子协程,这里CompletableDeferred只保证启动协程,并不保证运行协程,协程可能在实际运行前被取消

复制代码
class CoroutinesBasicsTest {

    @Test
    fun runCoroutinesExample() = runBlocking {
        println("------------------------------------------------------------------------------------------------------------------------------------------------------")
        // Used as a signal that the child coroutines have been launched
        val childrenLaunched = CompletableDeferred<Unit>()

// Launches two child coroutines
        val parentJob = launch {
            launch {
                println("Child coroutine 1 has started running")
                try {
                    awaitCancellation()
                } finally {
                    println("Child coroutine 1 has been canceled")
                }
            }
            launch {
                println("Child coroutine 2 has started running")
                try {
                    awaitCancellation()
                } finally {
                    println("Child coroutine 2 has been canceled")
                }
            }
            // Completes the CompletableDeferred,
            // signaling that the child coroutines have been launched
            childrenLaunched.complete(Unit)
        }
// Waits for the parent coroutine to signal that it has launched
// all of its children
        childrenLaunched.await()

// Cancels the parent coroutine, which cancels all its children
        parentJob.cancel()
        println("------------------------------------------------------------------------------------------------------------------------------------------------------")
    }
}

Child coroutine 1 has started running
Child coroutine 2 has started running
Child coroutine 1 has been canceled
Child coroutine 2 has been canceled

取消的作用

挂起点

当协程被取消时,它会继续运行,直到它到达代码中可能挂起的点,也称为挂起点

挂起函数内部使用suspendCancellableCorotine()检查它是否已被取消。如果有,协程将停止并抛出CancellationException

如下是常见的挂起函数

  • 单纯只是想演示"挂起直到取消" → awaitCancellation()

  • 等超时/等一段时间 → delay()

  • 等另一个协程发数据过来 → channel.receive()

  • 等另一个协程算出一个结果 → deferred.await()

  • 等锁被释放(并发控制) → mutex.lock()

    class CoroutinesBasicsTest {

    复制代码
      @Test
      fun runCoroutinesExample() = runBlocking {
          println("------------------------------------------------------------------------------------------------------------------------------------------------------")
          withContext(Dispatchers.Default) {
              val childJobs = listOf(
                  launch {
                      // Suspends until canceled
                      awaitCancellation()
                  },
                  launch {
                      // Suspends until canceled
                      delay(Duration.INFINITE)
                  },
                  launch {
                      val channel = Channel<Int>()
                      // Suspends while waiting for a value that's never sent
                      channel.receive()
                  },
                  launch {
                      val deferred = CompletableDeferred<Int>()
                      // Suspends while waiting for a value that's never completed
                      deferred.await()
                  },
                  launch {
                      val mutex = Mutex(locked = true)
                      // Suspends while waiting for a mutex that remains locked indefinitely
                      mutex.lock()
                  }
              )
    
              // Gives the child coroutines time to start and suspend
              delay(100.milliseconds)
    
              // Cancels all child coroutines
              childJobs.forEach { it.cancel() }
          }
          println("All child jobs completed!")
          println("------------------------------------------------------------------------------------------------------------------------------------------------------")
      }

    }

yield()

协程在线程中顺序执行,若一个协程没有挂起,它无法响应取消,且其他协程无法在相同线程中执行

长时间运行而没有挂起的代码中,定期调用yield()函数,为其他协程提供运行计划,并定期检查取消情况

复制代码
class CoroutinesBasicsTest {

    @Test
    fun runCoroutinesExample() = runBlocking {
        println("------------------------------------------------------------------------------------------------------------------------------------------------------")
        runBlocking {
            val coroutineCount = 5
            repeat(coroutineCount) { coroutineIndex ->
                launch {
                    val id = coroutineIndex + 1
                    repeat(5) { iterationIndex ->
                        val iteration = iterationIndex + 1
                        // Suspends temporarily to give other coroutines a chance to run
                        // Without this, the coroutines run sequentially
                        yield()
                        // Prints the coroutine index and iteration index
                        println("$id * $iteration = ${id * iteration}")
                    }
                }
            }
        }
        println("------------------------------------------------------------------------------------------------------------------------------------------------------")
    }
}

1 * 1 = 1
2 * 1 = 2
3 * 1 = 3
4 * 1 = 4
5 * 1 = 5
1 * 2 = 2
2 * 2 = 4
3 * 2 = 6
4 * 2 = 8
5 * 2 = 10
......

主动检查是否被取消

  • 取消协程时,isActive属性返回false
  • 当协程被取消时,ensureActive()函数会抛出CancellationException

利用协程取消中断线程

要在取消协程时中断线程,请将阻塞代码包装在runInterruptible()

复制代码
class CoroutinesBasicsTest {

    @Test
    fun runCoroutinesExample() = runBlocking {
        println("------------------------------------------------------------------------------------------------------------------------------------------------------")
        withContext(Dispatchers.Default) {
            val childStarted = CompletableDeferred<Unit>()
            val childJob = launch {
                try {
                    // Cancellation triggers a thread interruption
                    runInterruptible {
                        childStarted.complete(Unit)
                        try {
                            // Blocks the current thread for a very long time
                            Thread.sleep(Long.MAX_VALUE)
                        } catch (e: InterruptedException) {
                            println("Thread interrupted (Java): $e")
                            throw e
                        }
                    }
                } catch (e: CancellationException) {
                    println("Coroutine canceled (Kotlin): $e")
                    throw e
                }
            }
            childStarted.await()

            // Cancels the coroutine and interrupts the thread executing Thread.sleep()
            childJob.cancel()
        }
        println("------------------------------------------------------------------------------------------------------------------------------------------------------")
    }
}

Thread interrupted (Java): java.lang.InterruptedException: sleep interrupted
Coroutine canceled (Kotlin): kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job="coroutine#2":StandaloneCoroutine{Cancelling}@7afec4b2

取消时处理资源

协程执行到下一个挂起点时检测到取消,会立即抛出 CancellationException,不再继续执行后面的代码块,相关资源应该利用try-finally释放

复制代码
class CoroutinesBasicsTest {

    // 模拟一个"数据库连接",实现 AutoCloseable,方便统一管理
    class FakeDatabaseConnection(private val id: Int) : AutoCloseable {
        init {
            println("[DB-$id] 连接已打开")
        }

        suspend fun query(userId: String): String {
            println("[DB-$id] 开始查询用户 $userId ...")
            delay(1.seconds) // 模拟查询耗时,这是一个挂起点
            println("[DB-$id] 查询完成")
            return "User($userId)"
        }

        override fun close() {
            println("[DB-$id] 连接已关闭")
        }
    }

    fun openDatabaseConnection(id: Int): FakeDatabaseConnection = FakeDatabaseConnection(id)

    // ------------------------------------ 有坑的版本 ------------------------------------
    suspend fun loadUserProfileBad(scope: CoroutineScope, userId: String): Job {
        return scope.launch {
            val db = withContext(Dispatchers.IO) {
                openDatabaseConnection(1)
            }
            // 查询过程中如果被取消,下面 db.close() 永远不会执行
            val user = db.query(userId)
            println("Bad 版本更新 UI:$user")
            db.close()
        }
    }

    // ------------------------------------ 正确的版本:用 finally 保证一定会关闭 ------------------------------------
    suspend fun loadUserProfileGood(scope: CoroutineScope, userId: String): Job {
        return scope.launch {
            var db: FakeDatabaseConnection? = null
            try {
                db = withContext(Dispatchers.IO) {
                    openDatabaseConnection(2)
                }
                val user = db.query(userId)
                println("Good 版本更新 UI:$user")
            } finally {
                // 不管协程是正常走完,还是中途被取消,这里都会执行
                db?.close()
            }
        }
    }

    suspend fun main() {
        withContext(Dispatchers.Default) {
            println("===== 演示有坑的版本 =====")
            val badJob = loadUserProfileBad(this, "u001")
            delay(300.milliseconds) // 让协程先跑起来,db 已经打开,但查询还没完成
            badJob.cancel() // 取消:db.close() 永远不会跑到
            badJob.join()

            println()
            println("===== 演示正确的版本 =====")
            val goodJob = loadUserProfileGood(this, "u002")
            delay(300.milliseconds)
            goodJob.cancel() // 取消:finally 里的 db.close() 依然会执行
            goodJob.join()
        }
    }

    @Test
    fun runCoroutinesExample() = runBlocking {
        println("------------------------------------------------------------------------------------------------------------------------------------------------------")
        main()
        println("------------------------------------------------------------------------------------------------------------------------------------------------------")
    }
}

不可取消代码块

协程一旦被取消,即使在 finally 块里,只要还调用了挂起函数,依然会被取消打断

需要确保某些操作完成时(例如使用suspend的close()函数关闭资源),可以使用withContext(NonCancellable){}

复制代码
class CoroutinesBasicsTest {

    val serviceStarted = CompletableDeferred<Unit>()

    fun startService() {
        println("Starting the service...")
        serviceStarted.complete(Unit)
    }

    suspend fun shutdownServiceAndWait() {
        println("Shutting down...")
        delay(100.milliseconds)
        println("Successfully shut down!")
    }

    suspend fun main() {
        withContext(Dispatchers.Default) {
            val childJob = launch {
                startService()
                try {
                    awaitCancellation()
                } finally {
                    withContext(NonCancellable) {
                        // Without withContext(NonCancellable),
                        // this function doesn't complete because the coroutine is canceled
                        shutdownServiceAndWait()
                    }
                }
            }
            serviceStarted.await()
            childJob.cancel()
        }
        println("Exiting the program")
    }

    @Test
    fun runCoroutinesExample() = runBlocking {
        println("------------------------------------------------------------------------------------------------------------------------------------------------------")
        main()
        println("------------------------------------------------------------------------------------------------------------------------------------------------------")
    }
}

超时

超时允许您在指定的持续时间后自动取消协程,要指定超时,请使用withTimeoutOrNull(),超时返回null

复制代码
class CoroutinesBasicsTest {

    suspend fun slowOperation(): String {
        try {
            delay(300.milliseconds)
            return "A"
        } catch (e: CancellationException) {
            println("The slow operation has been canceled: $e")
            throw e
        }
    }

    suspend fun fastOperation(): String {
        try {
            delay(15.milliseconds)
            return "B"
        } catch (e: CancellationException) {
            println("The fast operation has been canceled: $e")
            throw e
        }
    }

    suspend fun main() {
        withContext(Dispatchers.Default) {
            val slow = withTimeoutOrNull(100.milliseconds) {
                slowOperation()
            }
            println("The slow operation finished with $slow")
            val fast = withTimeoutOrNull(100.milliseconds) {
                fastOperation()
            }
            println("The fast operation finished with $fast")
        }
    }
    @Test
    fun runCoroutinesExample() = runBlocking {
        println("------------------------------------------------------------------------------------------------------------------------------------------------------")
        main()
        println("------------------------------------------------------------------------------------------------------------------------------------------------------")
    }
}

The slow operation has been canceled: kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 100 ms
The slow operation finished with null
The fast operation finished with B
相关推荐
wifi___6 小时前
全局异常处理的原理
java·开发语言
2601_9638702010 小时前
【计算机毕业设计】基于Spring Boot的专科医院医疗管理系统
java·spring boot·课程设计
Bingo_BIG10 小时前
Java Spring 批量修改,实体、接口、方法的定义
java·spring
画中有画11 小时前
软件架构中质量属性(性能、安全、可扩展性)的权衡设计
java·运维·安全
Shaoxi Zhang11 小时前
JAVA学习笔记035——对象和JSON格式
java·笔记·学习
赵丙双11 小时前
CountDownLatch 源码分析
java·aqs·countdownlatch
余额瞒着我当琳11 小时前
C++--深拷贝三件套 + swap + 写时拷贝 + vector 扩容 + reserve
java·开发语言·c++
thefool11226611 小时前
翻转二叉树
java
喜欢打篮球的普通人12 小时前
LLVM Backend Lowering 从入门到实战:把 IR 变成机器码的完整链路
android·java·数据库