协程是一种轻量级的、非抢占式的多任务处理方式,是一种在单线程内执行多个任务的技术。与线程不同,线程需要操作系统管理,而协程由程序本身管理。
协程允许在单个线程内同时执行多个任务,并且每个任务可以暂时挂起,等待其他任务完成,从而避免了线程阻塞的问题。协程可以通过简单的方式实现多任务处理,并且可以更加高效和灵活地进行任务调度。
协程的基本概念包括:
- 任务:每个协程都是一个任务,任务可以是并行执行的。
- 切换:当一个任务需要等待其他任务完成时,它可以让出执行权,从而允许其他任务继续执行。
- 调度:协程的调度是在用户态完成的,不需要操作系统的支持,并且可以更加灵活地进行任务调度。
- 协作:协程之间可以协作执行,可以通过通道(channel)或者其他同步机制来实现任务之间的协作。
协程和线程区别
- 资源占用:线程是操作系统级别的资源,需要占用大量的系统资源,而协程是用户级别的资源,占用的系统资源比线程少,更适合在大量并发的场景中使用。
- 创建和销毁:创建协程的代价比创建线程低,销毁协程的代价也比销毁线程低,适合在大量创建和销毁的场景中使用。
- 切换方式:协程是协作式的,由程序本身主动请求切换,更加灵活和高效;线程是抢占式的,需要操作系统进行线程切换,更适合在强制执行任务顺序的场景中使用。
- 同步机制:协程之间可以使用更加简单的同步机制进行协调,如协程间的通信机制;线程之间需要使用更加复杂的同步机制进行协调,如锁、信号量等。
总体来说,协程比线程更适合用于实现并发、轻量级的任务处理,线程更适合用于强制执行任务顺序的场景。
suspend function:挂起函数
-
suspend 方法是指在协程中能够被挂起的方法,它的执行将会暂停直到其完成,但不会阻塞线程。
-
suspend 用于暂停执行当前协程,并保存所有局部变量。如需调用suspend函数,只能从其他suspend 函数进行调用,或通过使用协程构建器(例如 launch)来启动新的协程。
-
常使用的一些 suspend function:
-
delay()
-
用于在协程中延迟执行一段时间。它允许您在不阻塞线程的情况下暂停协程的执行。在延迟期间,其他协程可以继续执行。
-
yield()
-
放弃当前协程的占用权,它可以暂停协程的执行,并将执行权交给其他协程。
-
suspendCancellableCoroutine()
-
项目中常用的将回调改为协程的一个方法。
-
在完成或者错误时, 调用 continuation.resume()
-
注意:取消是协作的
-
一个典型的suspendCancellableCoroutine最佳实践是在异步操作完成时回调协程,如网络请求。下面是一个使用suspendCancellableCoroutine的示例代码:
-
suspend fun getResult(): String = suspendCancellableCoroutine { cont -> val callback = object : Callback { override fun onSuccess(result: String) { cont.resume(result) } override fun onError(error: Throwable) { cont.resumeWithException(error) } } makeNetworkRequest(callback) }
在上面的代码中,我们使用了suspendCancellableCoroutine函数,在makeNetworkRequest方法完成时,它将回调协程,并使用cont.resume(result)或cont.resumeWithException(error)在协程上恢复结果或异常。
CoroutineScope
表示协程运行的作用域,常用于与业务生命周期绑定。 业务可以在离开的时候对 CoroutineScope 进行 cancel。如:lifecycleScope
主要有以下分类:
1)顶级作用域:没有父协程的协程所在的作用域,也就是最顶层的作用域。在顶级作用域中,我们可以使用 GlobalScope 来创建协程,或者在 runBlocking 或者 coroutineScope 中创建协程。在顶级作用域中创建的协程是独立的,它们不受其他协程的影响,并且不受任何取消的影响。
2)协同作用域:指在已有的协程作用域中再创建一个协程作用域,并且它与原来的协程作用域共享相同的协程上下文(包括CoroutineDispatcher、Job等)。这样的目的是为了在当前协程作用域中启动一组相关的协程,并且可以一起结束。当前协程作用域中的协程可以使用coroutineScope函数来创建协同作用域,异常双向传播,异常会向上向下传播
3)主从作用域:指的是在一个协程中再创建另一个协程的形式,可通过supervisorScope创建。主协程的作用域成为主作用域,而从协程的作用域称为从作用域。当主作用域结束时,从作用域也会被取消,也就是说,从作用域是主作用域的一个子作用域。这种模式常用于组织一组相关的协程,以简化管理和取消这些协程的代码。异常自上而下传播,父协程不会受理子协程产生的异常,但是一旦父协程出现了异常,则会直接取消子协程。
import kotlinx.coroutines.*
fun main() = runBlocking<Unit> {
// 协同作用域
launch {
delay(1000L)
println("Task from runBlocking")
}
// 主从作用域
coroutineScope {
launch {
delay(500L)
println("Task from nested launch")
}
delay(100L)
println("Task from coroutine scope")
}
println("Coroutine scope is over")
}
//输出如下
Task from coroutine scope
Task from nested launch
Coroutine scope is over
Task from runBlocking
在上面的代码中,可以看到协同作用域是在runBlocking函数内启动的,而主从作用域是在coroutineScope内启动的。当coroutineScope完成时,主从作用域内的任务也会完成,但协同作用域不会受到影响。因此,主从作用域是在协同作用域内启动的一个子作用域,并且在主从作用域完成时,它的任务也会完成。
每一个 CoroutineBuilder 都需要 CoroutineScope, ScopeBuilder 有以下几种:
-
GlobalScope
-
是 worker 线程
-
GlobalScope(
object关键词修饰,其实就是个单例
)不受job任何边界限制。 -
GlobalScope用于启动顶级协程,在整个应用程序生命周期内运行且不会过早取消。
-
应用程序代码通常应使用应用程序定义的CoroutineScope。不建议GlobalScope在应用中使用。
-
MainScope
-
实现方式:
ContextScope(SupervisorJob() + Dispatchers.Main)
-
suspend coroutineScope
-
Creates a CoroutineScope and calls the specified suspend block with this scope. The provided scope inherits its coroutineContext from the outer scope, but overrides the context's Job.
-
会打破 context 的继承关系。
CoroutineContext
协程的上下文, 主要由以下四个 element 组成:
Job
- 协程的唯一标识,用来控制协程的生命周期(new、active、completing、completed、cancelling、cancelled);
- Job 是协程的句柄。使用
launch
或async
创建的每个协程都会返回一个 Job 实例(async 返回的是 Deferred,直接继承自 Job ),该实例唯一标识协程并管理其生命周期。 - Is part of its CoroutineContext:可以由 coroutineContext[Job] 来取出
- 主要 api
- 生命周期控制, start, cancel
- 状态回调:join, invokeOnCompletion
CoroutineDispatcher
-
指定协程运行的线程(IO、Default、Main、Unconfined);
-
Dispatchers.Main:主线程
-
Dispatchers.IO:专门优化适用于网络或磁盘IO
-
Dispatchers.Default:a shared background pool of threads,专门优化适用于 CPU 密集型作业
-
Dispatchers.Unconfined:
-
其他的都是 confined
-
哪个线程 invoke suspend 用哪个线程
-
适用于:非 UI, 非 CPU 密集事情
-
newSingleThreadContext:
-
Creates a coroutine execution context using a single thread with built-in yield support.
-
高成本的。需要考虑销毁或者复用问题
CoroutineName
指定协程的名称,默认为coroutine
CoroutineExceptionHandler
指定协程的异常处理器,用来处理未捕获的异常。
其他的还包括:NonCancellable ...
- NonCancellable 最好别用在 launch async 的参数中。
CoroutineBuilder
创建 Coroutine 的方法, 大都需要接收一个 CoroutineScope,可以自定义 CoroutineContext 去 override 父协程的一些设置。也可以定义一些 Start 来实现不同的效果
-
runBlocking
-
用于在协程的上下文中同步地执行代码块。"runBlocking"函数阻塞当前线程,直到代码块执行完毕。这意味着,在使用"runBlocking"函数时,不需要等待其他协程完成。
-
CoroutineScope.launch
-
最常用的构建器, 返回一个 Job,Job 中不携带任何自定义返回值,该对象可用于管理协程的生命周期,例如取消协程。
-
CoroutineScope.async
-
start a separate coroutine。
-
返回一个 Deferred
- 相对于 Job 的不同点是, 会返回一个结果
- async 返回一个 Deferred - a light-weight non-blocking future
- 可以使用 deferred.await() 来获取结果。
- 实际上也是一个 Job
- concurrency with coroutines is always explicit。
-
Async-style functions
- 可以在非协程中定义 async, 只是需要制定一个 Scope, 并且在协程体中 await
-
CoroutineScope.future
-
返回一个 Java 风格的 CompletableFuture
-
不支持 CoroutineStart.LAZY
-
CoroutineScope.produce
-
For Channel
主要接收参数
- CoroutineContext:override 父协程的一些 context, 常用于指定名字, 或者运行线程
- CoroutineStart:不是 CoroutineContext,用于指定运行方式
DEFAULT
立即进入待调度状态
LAZY
只有需要(start/join/await)时才开始调度
ATOMIC
和DEFAULT类似,且在第一个挂起点前不能被取消
UNDISPATCHED
立即在当前线程执行协程体,直到遇到第一个挂起点(后面取决于调度器)
Cancelling
-
取消是协作的,若需要响应 cancel,业务代码需要判断 isActive;
-
在 kotlinx.coroutines 中的所有 suspend 都是 cancellable 的;
-
父亲 cancel 了, 所有儿子 coroutine 都 cancel
fun main() { runBlocking { launch { delay(500) log("launch finish") } async { delay(500) log("async finish") } coroutineScope { delay(500) log("coroutineScope finish") } MainScope().launch { delay(500) log("MainScope finish") } cancel() } } // 输出如下 [main] coroutineScope finish Exception in thread "main" kotlinx.coroutines.JobCancellationException: BlockingCoroutine was cancelled; job=BlockingCoroutine{Cancelled}@1d119efb [AWT-EventQueue-0] MainScope finish
-
会在"协作"的地方产生一个 CancellationException, 如果 catch 住了, launch 不会停止
-
可以使用 withContext(NonCancellable){} 来使得块中的所有 suspend function 忽略已经取消的这个事情
- 注意, 不要在 launch 或 async 的参数中用 NonCancellable
-
经常在 finally 中, 做资源释放操作
try { repeat(1000) { i -> println("job: I'm sleeping $i ...") delay(500L) } finally { withContext(NonCancellable) { println("job: I'm running finally") delay(1000L) println("job: And I've just delayed for 1 sec because I'm non-cancellable") } }
Timeout
- 可以使用 withTimeout 来进行超时时间的设置
- Timeout 可能在任何时候出现, 需要合理掌握释放资源时机。
- 使用一个TimeoutCancellationException 来取消,是 CancellationException 的子类
Exceptions handling
Coroutine builder 分为两类,注意这两类的区分只是在 root 协程的声明时, 子协程不管是哪种声明都是向上传递 Exception:
fun main() = runBlocking {
val job = GlobalScope.launch { // root coroutine with launch
log("Throwing exception from launch")
throw IndexOutOfBoundsException() // Will be printed to the console by Thread.defaultUncaughtExceptionHandler
}
job.join()
log("Joined failed job")
val deferred = GlobalScope.async { // root coroutine with async
log("Throwing exception from async")
throw ArithmeticException() // Nothing is printed, relying on user to call await
}
try {
deferred.await()
log("Unreached")
} catch (e: ArithmeticException) {
log("Caught ArithmeticException")
}
}
//输出如下
[DefaultDispatcher-worker-1] Throwing exception from launch
[main] Joined failed job
[DefaultDispatcher-worker-1] Throwing exception from async
[main] Caught ArithmeticException
Exception in thread "DefaultDispatcher-worker-1" java.lang.IndexOutOfBoundsException
-
propagating exceptions automatically: launch & actor(废弃了)
-
会在 CoroutineExceptionHandler 中处理
-
exposing them to users: async & produce
- 会在 await() 或 receive() 的时候暴露
- 所有的 Exception 都放在 deferred 中
- CourtineExceptionHandler 没有作用
- 比较神奇的是,如果 try 一个子 async 的 await,也会 catch 住, 并且也会走 root 的 CoroutineExceptionHandler 和取消所有其他协程。
fun main() { runBlocking { GlobalScope.launch(CoroutineExceptionHandler { coroutineContext, throwable -> log("handler handle") }) { launch { log("start launch") delay(1000) log("launch finish") } val deferred = async { throw Exception("test") } kotlin.runCatching { deferred.await() }.getOrElse { log("catch exception") } }.join() } } //输出如下 [DefaultDispatcher-worker-2] start launch [DefaultDispatcher-worker-2] catch exception [DefaultDispatcher-worker-2] handler handle
-
可以在 context 中传递一个 CoroutineExceptionHandler
-
往往用于日志打印和异常处理, 不会无视这个 Exception 继续下面的逻辑
-
子会直接给父传递 Exception,所以非根协程的 Handler 无效
fun main() = runBlocking { GlobalScope.launch(CoroutineExceptionHandler { coroutineContext, throwable -> log("parent handler catch exception") }) { val job = launch(CoroutineExceptionHandler { coroutineContext, throwable -> log("child handler catch exception") }) { throw Exception("child throw exception") } job.join() }.join() } //输出如下 [DefaultDispatcher-worker-1] parent handler catch exception
-
async 会将所有 Exception 放在 await 中, 所以给 async 设置无效
-
遇到 Exception 时,整个流程是:
- 结束出 Exception 的当前 Coroutine
- cancel 根协程,进而 cancel 所有树中的协程 with that exception
- Exception 给到 handler 中(Async 给到 await 中)
-
Exceptions aggregation
- 在上述流程中如果仍出现 Exception,那么第一个出现的 Exception Wins。在异常产生后的流程中又产生的 Exception 被 attach 在第一个 Exception 后
-
监督作业(Supervisor)
-
只有取消向下传递,Exception 不向上传递,多个子 Coroutine 中一个发生异常, 不会取消其他的。
-
每个 child 协程解决自己的 exception
-
supervisorScope 同理
-
MainScope 就是其中一种