译文kotlin1.3 版本的协程
Posted yinhuanxu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了译文kotlin1.3 版本的协程相关的知识,希望对你有一定的参考价值。
原文链接:https://antonioleiva.com/coroutines/
协程是 kotlin 中让人激动的特性之一,使用协程,可以用一种优雅的方式来简化异步编程,让代码更加可读和易于理解。
使用协程,你可以用同步的方式写异步代码,而不是传统的 Callback 方式来写。同步方法的返回值就是异步计算的结果。
到底有什么魔力发生呢?我们马上即可学习它,在此之前,让我们了解下为什么协程很有必要。
协程是 kotlin1.1 推出的实验特性,在 kotlin1.3 版本发布了最终的 Api,现在已经可以投入生产。
Coroutines goal: The problem
假设你需要开发一个登录页面,UI 如下:
用户输入用户名和密码,然后点击登录按钮。
你的 App 代码实现里,需要向服务端发起请求来校验登录,然后请求该用户的好友列表,最后显示在屏幕上。
使用 kotlin 写出来的代码像这样:
progress.visibility = View.VISIBLE
userService.doLoginAsync(username, password) user ->
userService.requestCurrentFriendsAsync(user) friends ->
val finalUser = user.copy(friends = friends)
toast("User $finalUser.name has $finalUser.friends.size friends")
progress.visibility = View.GONE
这些步骤如下:
1.显示一个加载进度条
2.发送请求到服务端校验登录态
3.等待登录结果,再次请求好友列表
4.最后,隐藏掉加载进度条
但场景会越来越复杂,想象下这个 接口 还不是完善的,你获取好友列表数据后,还需要获取 推荐好友 列表数据,然后合并两个请求结果到一个单独的列表
有两种选择:
1.在好友列表请求完成后,请求推荐好友列表,这种方式是最简单的方式,但却不是高效的,第二个请求不需要等待第一个请求的结果。
2.同一时间发起两个请求,再同步两个结果,这种方式较为复杂。
在实际开发中,偷懒的程序员可能会选择第一种:
progress.visibility = View.VISIBLE
userService.doLoginAsync(username, password) user ->
userService.requestCurrentFriendsAsync(user) currentFriends ->
userService.requestSuggestedFriendsAsync(user) suggestedFriends ->
val finalUser = user.copy(friends = currentFriends + suggestedFriends)
toast("User $finalUser.name has $finalUser.friends.size friends")
progress.visibility = View.GONE
代码开始变得难以理解,我们看到了令人恐惧的嵌套回调,即下一个请求总是嵌套在了上一个请求的 callback 里。
kotlin 的 lambdas 表达式,让其不至于那么难看。但谁知道呢?将来你依然需要添加请求,使其变的越来越糟糕。
此外,别忘了我们这使用的是简单的方式,也就没那么高效了。
What are coroutines?
为了轻松的理解协程。我们可以说协程就像线程一样,但比线程更好。
首先,协程可以让你有顺序的写异步代码,大大的减轻了写异步代码的负担。
其次,它们更加的高效,多个协程可以在同一个线程上跑起来。App 可运行的线程数量是有限的,但是可运行的协程数量是近乎无限的
协程的基础是 suspending functions(中断函数)。中断函数可以在任意的时刻中断 协程 的运行,直到中断函数执行完成,或返回结果而结束
中断函数不会阻塞当前线程(通常情况下),我们说通常情况下,是因为取决于使用方式。具体下面会讲到。
coroutine
progress.visibility = View.VISIBLE
val user = suspended userService.doLogin(username, password)
val currentFriends = suspended userService.requestCurrentFriends(user)
val finalUser = user.copy(friends = currentFriends)
toast("User $finalUser.name has $finalUser.friends.size friends")
progress.visibility = View.GONE
在上面的例子中,是一个协程通用的结构。有一个协程 builder(构造器) ,和一些的 在返回结果前中断了 协程执行的中断函数
然后,你可以在下一行代码使用这个中断函数的返回结果,像极了有序的编码。kotlin中并不存在 coroutine 和 suspended 这两个关键字,上述例子我们先了解通用的结构
Suspending functions
中断函数(Suspending functions)可以在协程运行的时候中断其执行。当中断函数结束时,它的运行结果可以在下一行代码使用。
val user = suspended userService.doLogin(username, password)
val currentFriends = suspended userService.requestCurrentFriends(user)
中断函数可以运行在同一个 或者 不同的线程,但是中断函数仅可以运行在一个协程里,或者另一个中断函数里。
将函数声明为中断函数,只需要加上suspend关键字:
suspend fun suspendingFunction() : Int
// Long running task
return 0
回顾到最开始的案例,一个问题你可能会问,“这些代码是运行在哪个线程”。让我们先看到下面的代码
coroutine
progress.visibility = View.VISIBLE
...
这一行代码是在哪个线程运行的呢?你确定是运行在UI线程么?如果不是,你的App将会崩溃,这是一个很必要弄清楚的问题。
答案是:它依赖于coroutine context(协程的上下文)。
Coroutine context
协程的上下文,是用于定义协程怎么执行的规则和配置集合。它可以看作一个map,存储了一系列 keys 和values。
dispatcher 是其中一个配置,dispatcher 可以指定协程执行在哪个线程。
dispatcher 提供两种使用方式:
1.在需要使用的地方明确设置 dispatcher 类型。
2.通过协程的作用域(scope):关于scope在后面会细说
对于明确指定的使用方式,协程的构造器接收一个协程的上下文,作为第一个参数,我们可以直接将dispatcher当做第一个参数传进协程的构造器,dispatcher实现了CoroutineContext接口,因此可以这样使用:
coroutine(Dispatchers.Main)
progress.visibility = View.VISIBLE
...
现在,这行changes the visibility将会在UI线程执行,这个协程里的一切代码都是在UI线程执行都。那中断函数呢?
coroutine
...
val user = suspended userService.doLogin(username, password)
val currentFriends = suspended userService.requestCurrentFriends(user)
...
网络请求也是运行在UI线程么?如果是这样的情况的话,他们将会阻塞UI线程
中断函数在使用的时候,有不同的方式来配置 dispatcher。withContext是协程库提供的一个非常有用的函数。
withContext
这个函数让我们可以轻松的切换协程里部分代码执行的上下文。它是个中断函数,会中断协程的运行,直到中断函数执行完成
我们可以让中断函数切换到不同的线程:
suspend fun suspendLogin(username: String, password: String) =
withContext(Dispatchers.Main)
userService.doLogin(username, password)
这代码继续保持在主线程运行,因此它会阻塞UI,但我们可以使用不同的 dispatcher,轻松地切换。
suspend fun suspendLogin(username: String, password: String) =
withContext(Dispatchers.IO)
userService.doLogin(username, password)
现在,通过使用了 IO dispatcher,我们使用一个子线程去执行它,withContext本身是一个中断函数,因此我们没有必要使用它在另一个中断函数里,取而代之,我们可以这样做:
val user = withContext(Dispatchers.IO) userService.doLogin(username, password)
val currentFriends = withContext(Dispatchers.IO) userService.requestCurrentFriends(user)
你可能想了解我们有哪些 dispatchers 和什么时候使用,让我们来认识它们:
Dispatchers
正如你看见的,Dispatchers是一个线程上下文,可以指定协程里的代码运行在哪个线程。有的dispatchers仅使用一个线程,如main线程。其他的Dispatchers定义了一个线程池。将运行所有接收到的协程
如果你记得,最开始的时候,我们使用了一个线程来运行多个协程,因此系统不会为每一个协程都创建一个线程,但是会尝试复用已经存在的线程。
我们有四个主要的 Dispatchers
Default:当没有指定Dispatcher时它会被使用,我们可以明确的指定Dispatcher,这个Dispatcher可用于cpu密集型的计算任务。如一些计算,算法场景,它可以使用的线程数和cpu核心数一样多,对于密集型任务,cpu是很繁忙的,所以同时运行多个线程没有意义。
IO:当你需要运行 输入 / 输出 时会使用到它,通常的,当需要等待其他的系统回应时,即会阻塞线程的的任务,如服务端请求,读写数据库,文件,它不使用 cpu,可以同时让多个线程跑起来,是线程数为64的线程池。android的应用是设备和网络请求的交互,因此你可能更多的时候会使用到他们。
Unconfined:如果你不关心线程的使用,你可以使用这个。它很难控制线程的使用,因此当你不是很确定你要做什么的时候,不建议使用它。
Main:这是一个和UI关联的协程库里的特殊的Dispatcher,在Android中,他会使用UI线程。
你现在已经可以灵活的使用Dispatcher了。
Coroutine Builders
现在你能够很轻松的切换执行线程,你需要学习怎么创建一个新的协程,当然是使用协程构造器。
我们可以根据场景,选择不同的协程构造器,你也可以自定义自己的协程构造器,通常情况下,协程库提供给我们的已经足够了,让我们来看看:
runBlocking
这个协程构造器会阻塞当前的线程,直到在协程里所有的任务都执行完毕,这违背了我们使用协程的初衷。所以他有什么用处呢?
runBlocking对于测试中断函数非常有用,在你的测试程序里,runBlocking代码体里包含了中断函数,你可以断言结果,防止在中断函数结束之前,测试线程提前结束。
fun testSuspendingFunction() = runBlocking
val res = suspendingTask1()
assertEquals(0, res)
除了这个场景,你可能没有更多的地方需要用到runBlocking
launch
这是主要的构造器,你会经常使用它,因为它是创建协程最简单的方式,区别于runBlocking,它不阻塞当前线程(前提是正确使用了dispatchers)
这个构造器总是需要作用域的,在接下来会学习到作用域,在那之前,我们先使用GlobalScope。
GlobalScope.launch(Dispatchers.Main)
...
launch会返回一个Job对象,一个实现了CoroutineContext的类。
Jobs有几个有用的方法很有用,需要重点了解的是,一个job有一个父job,这个父 job可以控制子job
接下来介绍下Job的方法:
job.join
对于这个方法,可以阻塞当前job关联的协程,直到所有的子jobs结束。所有在协程里边调用的中断函数,都绑定了这个job。当所有子job结束后,当前协程才继续执行。
val job = GlobalScope.launch(Dispatchers.Main)
doCoroutineTask()
val res1 = suspendingTask1()
val res2 = suspendingTask2()
process(res1, res2)
job.join()
job.join()本身是一个中断函数,因此你需要在协程里调用它
job.cancel
这个函数可以取消与其关联的所有子jobs,举个例子,suspendingTask1在cancel()回调时是正在运行的,res1不会返回,suspendingTask2()也不会执行了。
val job = GlobalScope.launch(Dispatchers.Main)
doCoroutineTask()
val res1 = suspendingTask1()
val res2 = suspendingTask2()
process(res1, res2)
job.cancel()
job.cancel()是一个普通的函数,它不必在协程中调用。
async
这个构造器,可以解决最开始案例中的重要的问题。
async允许并行执行多个子线程,它不是一个中断函数,因为当我们使用 async 创建的协程启动时,下一行代码会立刻执行。async总是需要在协程里调用,它返回一个特殊的job,叫做Deferred。
这个对象有一个新的函数叫await(),它是一个中断函数。我们仅当需要结果时使用await(),如果这个结果还没准备好,这个协程会在这个时间点中断掉,如果我们已经准备好了结果,会返回结果并继续执行
因此在下面的例子,第二个请求和第三个请求需要等待第一个请求。但两个好友的请求是可以并行完成的,使用withContext,浪费了宝贵的时间。
GlobalScope.launch(Dispatchers.Main)
val user = withContext(Dispatchers.IO) userService.doLogin(username, password)
val currentFriends = withContext(Dispatchers.IO) userService.requestCurrentFriends(user)
val suggestedFriends = withContext(Dispatchers.IO) userService.requestSuggestedFriends(user)
val finalUser = user.copy(friends = currentFriends + suggestedFriends)
我们想象下每个请求要花2秒,那么这将要花6秒才结束,如果我们使用async来替代的话:
GlobalScope.launch(Dispatchers.Main)
val user = withContext(Dispatchers.IO) userService.doLogin(username, password)
val currentFriends = async(Dispatchers.IO) userService.requestCurrentFriends(user)
val suggestedFriends = async(Dispatchers.IO) userService.requestSuggestedFriends(user)
val finalUser = user.copy(friends = currentFriends.await() + suggestedFriends.await())
第二个和第三个并行执行,他们将会在同一时间运行,这样时间将减少至4秒。
除此之外,同步两个结果是简单的,只需要调用两者的await,让协程框架完成。
Scopes
到目前为止,我们有一套很不错的代码,使用简单的方式来解决一些复杂的场景。但我们仍然还有一个问题。
想象下我们需要显示好友列表在一个RecyclerView,但是当我们运行在其中一个后台任务时,这个用户关闭了Activity,这个Activity将会isFinishing状态,因此所有的UI更新都会抛出异常。
我们可以怎样解决这种场景呢?使用Scopes。我们看下各种Scopes的用法。
Global scope
这是一个常用scope,当协程的生命周期和App的生命周期一样长久的话,使用这个scope,因此他们不应该和可以销毁的组件绑定。我们在上述使用过它,因此现在应该简单了。
GlobalScope.launch(Dispatchers.Main)
...
当你使用GlobalScope,总是需要问自己这个协程是不是伴随整个App生命周期的。而不是仅仅一个页面或组件。
Implement CoroutineScope
所有的类都可以实现这个接口(CoroutineScope)成为一个作用域,这个仅仅需要重写 coroutineContext属性。
这里,有至少两个重要的东西需要配置,dispatcher和job
你需要记住,一个context可以组合多个context,组合的context需要不同的类型,所以这里,通常的,你将定义两个东西,
dispatcher,用于指定协程的dispatcher
job,可以在任何时候取消协程。
class MainActivity : AppCompatActivity(), CoroutineScope
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
private lateinit var job: Job
这个plus(+)操作符在组合context时使用,如果两个不同类型的context组合时。会创建一个CombinedContext,CombinedContext拥有两个上下文的配置。
另一方面,如果两个相同类型context组合,新的上下文使用第二个上下文的配置,即 instance:Dispatchers.Main + Dispatchers.IO == Dispatchers.IO
我们创建job可以使用了lateinit,在onCreate延迟初始化它。它将在onDestroy时被取消。
override fun onCreate(savedInstanceState: Bundle?)
super.onCreate(savedInstanceState)
job = Job()
...
override fun onDestroy()
job.cancel()
super.onDestroy()
现在,当使用协程时,代码变的简单。你可以使用构造器,跳过协程的context,因为已经在自定义作用域定义了包含 main dispatcher的上下文。
launch
...
当然,如果你的activitiy里使用协程,将其提取到父类是很值得的
补充1 - 从 callbacks 转为协程
如果你开始考虑将协程应用到你的项目中,你可能会考虑将当前使用callback的代码,转为协程。
suspend fun suspendAsyncLogin(username: String, password: String): User =
suspendCancellableCoroutine continuation ->
userService.doLoginAsync(username, password) user ->
continuation.resume(user)
suspendCancellableCoroutine方法返回一个continuation对象,这个对象可以返回callback的结果。只需要调用continuation.resume。结果将会通过中断函数返回给父协程。
补充2 - 关于协程与 Rxjava
是的,提到协程,我也有相同的问题:"协程可以替代Rxjava么?"答案是不。
如果你使用Rxjava仅是用于从主线程切换到子线程,你发现协程可以更加简单的完成这工作,是的,你可以不需要Rxjava
如果你使用流式操作,转换流,合并流等,Rxjava依然做的更出色,在协程有一个叫Channels 的东西能够替代Rxjava的多数简单使用场景,但是Rxjava流式操作更加让人喜欢
值得一提的是kotlin有一个开源库,可以在协程里使用rxjava。
总结
协程提供了更多的可能性,以一种你可能没想到的方式来简化异步编程。
赶快来体验协程吧!
以上是关于译文kotlin1.3 版本的协程的主要内容,如果未能解决你的问题,请参考以下文章
“无法访问主线程上的数据库,因为它可能会长时间锁定 UI。”我的协程错误
go语言学习笔记 — 进阶 — 并发编程:go语言的协程goroutine,与普通程序的协程coroutine