本文翻译自 Sean McQuillan 的 Kotlin coroutines 入门系列。看了他的三篇文章,真正了解了协程出现的意义,它能帮开发者解决的问题,并掌握了它的基本用法。 原文地址:html
对于 Android 开发者来讲,咱们能够将协程运用在如下两个场景:android
suspend
函数来执行一些操做而不阻塞主线程。咱们都知道不管是请求网络仍是读取数据库都是耗时任务,咱们不能在主线程去执行这些耗时操做。如今的手机 CPU 频率都是很高的,Pixel 2 的单核 CPU 周期小于 0.0000000004 秒(0.4纳秒),而一次网络请求大约是 0.4 秒(400 毫秒)。能够这么说,一眨眼功夫能够完成一次网络请求,但同时 CPU 已经执行了 10 亿屡次。 Android 平台,主线程是 UI 线程,主要负责 View 的绘制(16 ms)和响应用户操做。若是咱们在主线程作耗时操做,就会阻塞主线程,形成 View 不能及时刷新,不能及时响应用户操做,从而影响用户体验。 为了解决以上问题,咱们通常使用 Callbacks 的方式。举个例子:git
class ViewModel: ViewModel() {
fun fetchDocs() {
get("developer.android.com") { result ->
show(result)
}
}
}
复制代码
尽管 get()
函数是被主线程调用的,但它的实现确定是要在其余线程完成网络请求的。当结果返回时,Callback 又会在主线程被调用,来将结果显示到 UI 上。github
协程能够简化异步代码,用协程咱们能够更方便地重写上面的例子:数据库
// Dispatchers.Main
suspend fun fetchDocs() {
// Dispatchers.IO
val result = get("developer.android.com")
// Dispatchers.Main
show(result)
}
// look at this in the next section
suspend fun get(url: String) = withContext(Dispatchers.IO){/*...*/}
复制代码
与普通函数相比,协程添加了 suspend
和 resume
两种操做。这两个操做一块儿完成了 Callback 的工做,但更优雅,就像是用同步代码完成了异步操做。编程
suspend
:挂起当前协程,保存全部的本地变量;resume
:恢复已经挂起的协程,从它暂停的地方继续执行。
suspend
是 Kotlin 的一个关键字。被suspend
标记的函数,只能在suspend
函数内被调用。咱们可使用协程提供的launch
和async
从主线程启动一个协程来执行suspend
函数。安全
public fun CoroutineScope.launch( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
public fun <T> CoroutineScope.async( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> T
): Deferred<T> {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyDeferredCoroutine(newContext, block) else
DeferredCoroutine<T>(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
复制代码
如动画所示,get()
函数在执行前会挂起(suspend)当前的协程,它内部依旧是经过 IO 线程(Dispatchers.IO
)来执行网络请求。当请求完成时,它不是经过 Callback 的方式,而是恢复(resume)已经挂起的协程继续执行 show(result)
函数。任何一个协程被挂起时,当前的栈信息都会被复制并保存,以便在恢复时使用。当全部的协程都被挂起时,主线程不会被阻塞,仍然能够更新 UI 和响应用户操做。因而可知,协程为咱们提供了一种异步操做的简单实现方式。网络
使用 Kotlin 协程的一个原则是:咱们应该保证咱们写的 suspend
函数是主线程安全的,也就是能够在任何线程中调用它,而不用去让调用者手动切换线程。 须要注意的是:suspend
函数通常是运行在主线程中的,suspend
不是意味着运行的子线程。也就是说,咱们须要在 suspend
内部指定该函数执行的线程,如不指定,它默认运行在调用者的线程。 若是不是执行耗时任务,咱们可使用 Dispatchers.Main.immediate 来启动一个协程,下一次 UI 刷新时就会将结果显示到 UI 上。 全部的 Kotlin协程都必须运行在一个 Dispatcher
中,它提供了如下几种 Dispatcher
来运行协程。并发
Dispatchers | 用途 | 使用场景 |
---|---|---|
Dispatchers.Main | 主线程、UI交互、执行轻量任务 | Call suspend functions, Call UI functions, Update LiveData |
Dispatchers.IO | 网络请求、文件访问 | Database, Reading/writing files, Networking |
Dispatchers.Default | CPU密集型任务 | Sorting a list, Parsing JSON, DiffUtils |
Dispatchers.Unconfined | 不限制任何指定线程 | 限制恢复后的线程 |
完整的 get()
函数以下所示:app
// Dispatchers.Main
suspend fun fetchDocs() {
// Dispatchers.Main
val result = get("developer.android.com")
// Dispatchers.Main
show(result)
}
// Dispatchers.Main
suspend fun get(url: String) =
// Dispatchers.IO
withContext(Dispatchers.IO) {
// Dispatchers.IO
/* perform blocking network IO here */
}
// Dispatchers.Main
复制代码
使用协程咱们能够自由控制代码运行的线程,withContext()
为咱们提供了相似编写同步代码的方式来实现异步编程。如上面提到的:咱们应尽可能使用 withContext
确保每一个函数都是线程安全的,不要让调用者关心要在哪一个线程才能调用该函数。 上面的例子中,fetchDocs
运行在主线程,而 get()
运行在子线程,因为协程的 挂起/恢复 机制,当 withContext
返回时,当前协程会恢复执行。 在性能方面,withContext
跟 Callbacks 或 RxJava 不相上下。因此咱们不用担忧性能问题,相信官方也会持续优化。
协程相比于线程来讲,它是很轻量的。咱们能够启动上百上千个协程,但没法启动这么多的线程。虽然协程很轻量,但它们的实际进行的任务多是耗时的,好比用于读取数据库、请求网络或读写文件的协程。所以,咱们仍须要维护好这些协程的完成和取消,不然可能发生任务泄露,这比内存泄漏更严重,由于任务可能浪费 CPU、磁盘或网络资源。 手动管理成百上千个协程是很困难的,为了不协程泄露,Kotlin 提供了 结构化并发 来帮助咱们更方便地追踪全部运行中的协程。在 Android 开发过程当中,咱们能够用它来完成如下三件事:
Kotlin 协程必须运行在 CoroutineScope
中,CoroutineScope
能够追踪全部运行中和已挂起的协程,不像上文提到的 Dispatchers
,它只是保证对全部的协程的追踪,而不会真正地执行它们。所以为了确保全部的协程都能被追踪到,咱们不能在 CoroutineScope
外启动一个新的协程。同时咱们可使用 CoroutineScope
来取消在它内部启动的全部协程。 咱们须要在普通函数中启动一个协程,才能调用 suspend
函数。协程提供了两种方式来启动一个新的协程。
大多数状况下,咱们使用 launch
来启动一个新的协程。launch
函数就像链接普通函数和协程的桥梁。
scope.launch {
// This block starts a new coroutine
// "in" the scope.
//
// It can call suspend functions
fetchDocs()
}
复制代码
launch
和 async
最大的不一样就是它们处理异常的方式:launch
启动的协程在发生异常时会马上抛出,并马上取消全部协程;而 async
启动的协程,只有咱们调用 await()
函数时才能获得内部的异常,若无异常会返回执行结果。
AndroidX Lifecycle KTX 为咱们提供了 viewModelScope
来方便地在 ViewModel
中启动协程,并保持对它们的追踪。
class MyViewModel(): ViewModel() {
fun userNeedsDocs() {
// Start a new coroutine in a ViewModel
viewModelScope.launch {
fetchDocs()
}
}
}
复制代码
咱们能够在一个 CoroutineScope
中包含若干个 CoroutineScope
,若是咱们在一个协程中启动了另外一个协程,其实它们最终都同属于一个最顶层的 CoroutineScope
,也就是说咱们能够经过取消最外层的协程来取消全部内部的协程。 若是咱们取消一个已经挂起的协程,它会抛出一个异常 CancellationException
。若是咱们捕获并消费了这个异常,或者取消一个未挂起的协程,该协程会处于一个 半取消(semi-canceled)状态。 viewModelScope
启动的协程会在 ViewModel 销毁(clear)时自动取消,因此即便咱们其内部执行是一个死循环,也会被自动取消。
fun runForever() {
// start a new coroutine in the ViewModel
viewModelScope.launch {
// cancelled when the ViewModel is cleared
while(true) {
delay(1_000)
// do something every second
}
}
}
复制代码
咱们可使用协程进行网络请求、读写数据库等耗时操做。但有时咱们可能须要在一个协程中同时进行两个网络请求,这时咱们须要再启动两个协程来共同工做。咱们能够在任何一个 suspend
函数中使用 coroutineScope
或 supervisorScope
来启动更多的协程。 在一个协程中启动新的协程可能会形成潜在的任务泄露,由于调用者可能不知道咱们内部的实现。好消息是,结构化并发能够保证:若是一个 suspend
函数返回了,那么它内部的全部代码都已经执行完毕。 这仍然是同步调用的影子。 举个例子:
suspend fun fetchTwoDocs() {
coroutineScope {
launch { fetchDoc(1) }
async { fetchDoc(2) }
}
}
复制代码
如上所示:fetchTwoDocs()
内部经过 coroutineScope
又启动了两个协程来同时加载两个文档,一个方式是 launch
,一种方式是 async
。为了不 fetchTwoDocs()
任务泄露,coroutineScope
会一直保持挂起状态,直到内部的全部协程都执行完毕,这时 fetchTwoDocs()
函数才会返回。
以上示例,咱们同时启动了 1000 个协程来请求网络,loadLots()
内部的 coroutineScope
是 该函数调用者的 CoroutineScope 的子集,内部的 coroutineScope
会一直保持这 1000 个协程的追踪,只有当全部协程都执行完毕,loadLots()
函数才会返回。
coroutineScope
和 supervisorScope
可让咱们在任意 suspend
函数内安全启动协程,直到内部的全部协程都执行完毕,它们才会返回。此外,若是咱们取消了外层的 scope,内部的子协程也会被取消。 coroutineScope
和 supervisorScope
的区别是:只要 coroutineScope
内的任一协程执行失败,整个 scope 都会被取消,内部的其余子协程也会马上被取消;而 supervisorScope
内的某一协程失败,不会取消其余的子协程。
和普通函数同样,协程在执行失败时也会抛出异常。suspend
函数内抛出的异常是会向上传递的,咱们也可使用 try/catch
语法或其余方式捕获异常。可是下面这种异常可能会丢失:
val unrelatedScope = MainScope()
// example of a lost error
suspend fun lostError() {
// async without structured concurrency
unrelatedScope.async {
throw InAsyncNoOneCanHearYou("except")
}
}
复制代码
以上代码中,咱们在一个不相关的限定范围内启动了一个协程,它并非结构化并发的。因为 async
函数启动的协程只有在调用 await()
时才会抛出异常,因此这个异常可能会丢失,它会被一直保存着直到咱们调用 await()
。 结构化并发能够保证当一个协程发生异常时,它的调用者或 scope 能够收到这个异常。 上面代码用结构化并发的方式改写以下:
suspend fun foundError() {
coroutineScope {
async {
throw StructuredConcurrencyWill("throw")
}
}
}
复制代码
总结一下:
coroutineScope
和 supervisorScope
是结构化并发的,能够追踪内部的全部协程,包括异常处理、任务取消等。GlobalScope
不是结构化并发的,它是一个全局的 scope,跟 Application 同生命周期。Kotlin Coroutines 与 RxJava&RxAndroid 均可以方便的帮咱们进行异步编程,我的以为它们在异步编程最大的区别是:Coroutines 的编写方式更像是同步调用,而 RxJava 是流式编程。但本质上,它们内部都是经过线程池来处理耗时任务。RxJava 的有不少个操做符能够辅助实现各式各样的需求,并能保证链式调用;Coroutines 是与 Kotlin 结合的最好异步编程方式,目前也有不少的官方支持,相信未来 Coroutines 会有很好的使用体验和执行性能。
我是 xiaobailong24,您能够经过如下平台找到我: