Kotlin 协程的取消与超时

取消可以请求协程在正常完成之前停止执行。当用户关闭窗口或离开页面,而协程仍在运行时,取消可以停止不再需要的工作。

取消也可以提早释放资源,避免协程继续访问已销毁的对象。对于反复执行工作的长时间任务,它同样适用,例如:

  • 发送心跳。
  • 执行定时任务。
  • 用最新读数更新状态,例如时钟界面。

取消通过 Job 句柄实现。它表示协程的生命周期及父子关系,可检查协程是否仍活跃,并按结构化并发的规则取消协程及其子协程。

取消协程

在协程的 Job 句柄上调用 cancel(),即可取消协程。.launch() 等协程构建器返回 Job;.async() 返回的 Deferred 也实现了 Job,支持相同的取消行为。

既可以手动调用 cancel(),也可以让取消通过父协程自动传播。

被取消的协程在下一次检查取消时抛出 CancellationException。kotlinx.coroutines 的挂起函数,例如 delay(),会在挂起时检查取消。

awaitCancellation() 会让协程挂起,直到它被取消;相当于 delay(Duration.INFINITE)。具体检查时机见下文“挂起点与取消”。

手动取消协程的示例如下:

import kotlinx.coroutines.*

suspend fun main() {
//sampleStart
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")
//sampleEnd
}

这里用 CompletableDeferred 表示子协程已经开始运行。子协程启动时调用 complete(),而 await() 只有在它完成后才返回。取消协程并不必须执行这项检查;示例加入它,是为了确保取消之前子协程已开始运行并打印消息,让演示过程更容易复现。

Deferred 实现了 Job,所以 async() 创建的协程也采用相同的取消方式:

val deferred = async { /* ... */ }
deferred.cancel()

捕获 CancellationException 可能破坏取消传播。如果必须捕获,应重新抛出,让取消沿协程层次正确传播。详见协程异常处理。

取消传播

结构化并发保证,取消父协程时也会取消其所有子协程,避免子协程在父协程取消之后继续工作。例如:

import kotlinx.coroutines.*

suspend fun main() {
    withContext(Dispatchers.Default) {
//sampleStart
// 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()
//sampleEnd
    }
}

每个子协程都使用 finally,因而子协程被取消时会执行其中的代码。这里的 CompletableDeferred 只保证子协程已经被创建并启动调度,不保证它们已经开始执行。如果在执行之前就被取消,子协程不会打印任何消息。

让协程响应取消

Kotlin 的协程取消是协作式的。协程必须通过挂起或显式检查取消来配合,才能响应取消。

本节说明如何通过 yield() 等函数加入挂起点,使协程能够检查取消。

挂起点与取消

取消后,协程会继续执行,直到到达可能挂起的位置,也就是挂起点。如果确实在此挂起,挂起函数便会检查取消状态;若已取消,就停止协程并抛出 CancellationException。

调用 suspend 函数是一个挂起点,但不一定真的挂起。例如,等待 Deferred 的结果时,只有结果尚未完成才会挂起。

以下示例使用会挂起的常见函数,让协程能够检查取消并停止:

import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.channels.Channel
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration

suspend fun main() {
//sampleStart
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!")
//sampleEnd
}

kotlinx.coroutines 库的可取消挂起操作通过 suspendCancellableCoroutine() 等机制配合取消,在挂起时检查状态。相比之下,仅用 suspendCoroutine() 实现的自定义挂起函数,不会自动响应取消。

yield() 挂起函数

如果协程不挂起,同一线程上的其他协程就无法获得执行机会,只能顺序运行。长时间不挂起的协程也不会及时响应取消。

对于 CPU 密集型计算或其他长时间不挂起的代码,应定期调用 yield()。它让出当前线程,让其他协程有机会执行,同时检查取消;若已经取消,会抛出 CancellationException。

对比不检查取消、使用 ensureActive 或 isActive、使用 yield 的协程执行方式
对比不检查取消、使用 ensureActive 或 isActive、使用 yield 的协程执行方式

图中比较了三种情况:不检查取消时,任务持续占用线程;ensureActive() 或 isActive 可以检查取消,但不主动让出线程;yield() 同时提供取消检查与让出执行机会。此图来自官方文档,图中文字和绘制结果原样保留。

例如:

import kotlinx.coroutines.*

fun main() {
//sampleStart
// runBlocking uses the current thread for running all coroutines
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}")
            }
        }
    }
}
//sampleEnd
}

此例中,每个协程在两次迭代之间调用 yield(),让其他协程也能运行。它提供调度机会,不是固定打印顺序的保证。

显式检查取消

也可以显式检查取消,让长时间计算在不挂起的情况下响应取消。但不挂起的长任务仍可能阻止同一线程上的其他协程运行;除非这种行为符合需求,否则优先使用 yield()。

不同 API 的行为不同:

  • isActive 在协程被取消后返回 false。
  • ensureActive() 在协程被取消后抛出 CancellationException。

取消协程时中断阻塞代码

JVM 上的 Thread.sleep()、BlockingQueue.take() 等阻塞函数会阻塞当前线程。这些函数支持线程中断,可提前停止;但从协程中调用它们时,取消协程本身不会自动中断线程。

需要取消时中断线程,就把阻塞代码包装在 runInterruptible() 中:

import kotlinx.coroutines.*

suspend fun main() {
//sampleStart
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()
}
//sampleEnd
}

取消时安全处理取得的值

挂起中的协程被取消后,会以 CancellationException 恢复,而不是返回结果,即使结果已经准备好。这称为及时取消(prompt cancellation),可避免代码在已取消的作用域内继续执行,例如更新已关闭的界面。

例如:

// Defines a coroutine scope that uses the UI thread
class ScreenWithButtons(private val scope: CoroutineScope) {
    fun loadAndUpdateButtons(filename: String) {
        scope.launch {
            // withContext() checks for cancellation before entering the block
            // and after the block returns
            val buttonNames = withContext(Dispatchers.IO) {
                // This is a blocking call that doesn't react to cancellation
                readLines(filename)
            }
            
            // It's safe to call updateUi()
            // because withContext() doesn't return if the coroutine is canceled,
            // and no code running on the UI thread can dispose of the buttons before this call
            updateUi(buttonNames)
        }
    }

    // Call this function only from the UI thread because it accesses the buttons
    // Throws an exception if called after the buttons are disposed
    private fun updateUi(buttonNames: List<String>) {
        // Placeholder code that updates the buttons with specified names
    }

    // Call this function only from the UI thread
    fun leaveScreen() {
        // Cancels the scope when leaving the screen
        // You can no longer update the UI
        scope.cancel()
    }
}

// UI controller code
setHandler(Event.ScreenClosed) {
    // Runs on the UI thread
    screenWithButtons.leaveScreen()
    buttons.dispose()
}

withContext(Dispatchers.IO) 配合取消:若 leaveScreen() 在读取结果返回之前取消协程,updateUi() 就不会执行。该示例依赖界面操作都在同一 UI 线程这一前提,readLines、事件注册与更新界面的实现均为示意代码,不是完整可独立编译的应用。

及时取消可以避免使用失效的值,但也可能发生在某个重要值仍需清理时,导致资源丢失。例如,协程取得了 AutoCloseable 资源,却在运行关闭逻辑之前被取消。

因此,应将清理逻辑放在即使接收资源的协程被取消,也能保证执行的位置:

import java.nio.file.*
import java.nio.charset.*
import kotlinx.coroutines.*
import java.io.*

// Uses a scope that runs its coroutines on the UI thread
class ScreenWithFileContents(private val scope: CoroutineScope) {
    fun displayFile(path: Path) {
        scope.launch {
            // Stores the reader in a variable, so the finally block can close it
            var reader: BufferedReader? = null
            
            try {
                withContext(Dispatchers.IO) {
                    reader = Files.newBufferedReader(
                        path, Charset.forName("US-ASCII")
                    )
                }
                // Uses the stored reader after withContext() completes
                updateUi(reader!!)
            } finally {
                // Ensures the reader is closed even when the coroutine is canceled
                reader?.close()
            }
        }
    }

    private suspend fun updateUi(reader: BufferedReader) {
        // Shows the file contents
        while (true) {
            val line = withContext(Dispatchers.IO) {
                reader.readLine()
            }
            if (line == null)
                break
            addOneLineToUi(line)
        }
    }

    private fun addOneLineToUi(line: String) {
        // Placeholder for code that adds one line to the UI
    }

    // Only callable from the UI thread
    fun leaveScreen() {
        // Cancels the scope and prevents its coroutines from updating the UI
        scope.cancel()
    }
}

这里把 BufferedReader 保存在变量中,并在 finally 中关闭。即使协程被取消,也能释放资源。界面更新函数依然是示意实现。

运行不可取消的代码块

把 NonCancellable 传给 withContext(),可以让特定代码块不受取消影响。

不要把 NonCancellable 用在 .launch()、.async() 等其他构建器上;这会破坏父子关系和结构化并发。

当某些操作必须完成,例如调用会挂起的 close() 释放资源时,即使协程已经被取消,也可以使用 NonCancellable。例如:

import kotlinx.coroutines.*
import kotlin.time.Duration.Companion.milliseconds

//sampleStart
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")
}
//sampleEnd

超时

超时会在指定时间后自动取消协程,可用于停止耗时过长的操作。例如,下载图片超时后,可以重试或改用本地缓存。

使用带 Duration 参数的 withTimeoutOrNull() 设置超时:

import kotlinx.coroutines.*
import kotlin.time.Duration.Companion.milliseconds

//sampleStart
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")
    }
}
//sampleEnd

操作超过指定时长时,withTimeoutOrNull() 返回 null。超时依然依赖协作式取消,不能据此认为任意阻塞函数都能在截止时间被强制停止。

来源与许可

原文:Cancellation and timeouts,实际文档源位于 Kotlin/kotlinx.coroutines。Copyright 2000-2020 JetBrains s.r.o. and Kotlin Programming Language contributors。文档源、代码和配套教学图随仓库采用 Apache License 2.0,许可全文置于 assets/license.txt。

本稿将正文汉化,保留代码、字符串、英文注释和教学图原样,并补充示意代码、UI 线程及取消边界说明。代码经过静态核对,未在本机编译或运行。此次修改为中文翻译与说明补充。

许可全文(英文原文)
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright 2000-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容