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