我为什么要使用Kotlin的协程?
看来RxKotlin库具有更多的用途。与之相比,Kotlin的协程看起来强大得多,使用起来也比较麻烦。
我基于协程的观点基于Andrey Breslav(JetBrains)的设计演讲
演讲幻灯片可在此处访问。
编辑(感谢@hotkey):
在此可以更好地了解协程的当前状态。
我为什么要使用Kotlin的协程?
看来RxKotlin库具有更多的用途。与之相比,Kotlin的协程看起来强大得多,使用起来也比较麻烦。
我基于协程的观点基于Andrey Breslav(JetBrains)的设计演讲
演讲幻灯片可在此处访问。
编辑(感谢@hotkey):
在此可以更好地了解协程的当前状态。
Answers:
免责声明:由于Coroutines现在具有流程API,与Rx one非常相似,因此该答案的某些部分无关紧要。如果您想要最新的答案,请跳至上一个编辑。
Rx中有两个部分:可观察模式,以及一组可靠的运算符来进行操作,转换和组合。可观察模式本身并没有做什么用。与协程相同;这只是处理异步的另一范式。您可以比较回调,Observable和协程的优点/缺点来解决给定的问题,但是不能将范例与功能齐全的库进行比较。这就像将语言与框架进行比较。
Kotlin协程如何比RxKotlin更好?尚未使用协程,但它看起来类似于C#中的异步/等待。您只需编写顺序代码,除异步执行外,其他一切都与编写同步代码一样容易。更容易掌握。
为什么我要使用Kotlin协程?我会自己回答。大多数时候,我会坚持使用Rx,因为我更喜欢事件驱动的体系结构。但是在出现编写顺序代码的情况时,需要在中间调用异步方法,我会很乐意利用协程保持这种方式,并避免将所有内容包装在Observable中。
编辑:现在,我正在使用协程,是时候进行更新了。
RxKotlin只是在Kotlin中使用RxJava的语法糖,因此下面我将谈论RxJava而不是RxKotlin。与RxJava相比,协程是一个较低的杠杆,是更通用的概念,它们可用于其他用例。也就是说,在一个用例中,您可以比较RxJava和协程(channel
),它异步地传递数据。协程在这里比RxJava有明显的优势:
subscribeOn()
并ObserveOn()
混淆了。每个协程都有一个线程上下文并返回到父上下文。对于渠道,双方(生产者,消费者)都在自己的上下文中执行。协程在线程或线程池影响方面更为直观。yield
,对于给定的计算,您可以传递hand(),priorize (),priorize(select
),parallelize(multiple producer
/ actor
on channel
)或锁定资源(Mutex
)。在服务器上(RxJava排在第一位)可能并不重要,但是在资源有限的环境中,可能需要此级别的控制。send()
通道的另一端,是一个暂停功能,当达到通道容量时会暂停。这是自然产生的开箱即用的背压。您也可以offer()
进行频道设置,在这种情况下,调用不会挂起,而false
在频道已满的情况下返回,可以有效地onBackpressureDrop()
从RxJava复制。或者,您也可以编写自己的自定义背压逻辑,这对于协程并不难,尤其是与RxJava相比。还有另一个用例,协程会发光,这将回答您的第二个问题“我为什么要使用Kotlin协程?”。协程是后台线程或AsyncTask
(Android)的完美替代。就像一样简单launch { someBlockingFunction() }
。当然,您也可以使用RxJavaSchedulers
并使用来实现此目的Completable
。您不会(或很少)使用Observer模式和作为RxJava签名的运算符,这表明这项工作超出RxJava的范围。RxJava的复杂性(此处无用的税)将使您的代码比Coroutine的版本更冗长,更简洁。
可读性很重要。在这方面,RxJava和协程的方法相差很多。协程比RxJava简单。如果你不放心用map()
,flatmap()
和功能反应式编程一般,协程的操作更容易,涉及到基本的指令:for
,if
,try/catch
...但我个人觉得协同程序的代码更难理解为不平凡的任务。特别是它涉及更多的嵌套和缩进,而RxJava中的运算符链接使一切保持一致。函数式编程使处理更加明确。最重要的是,RxJava可以使用丰富的标准操作符(好的,太丰富了)来解决复杂的转换。当您具有需要大量组合和转换的复杂数据流时,RxJava会大放异彩。
我希望这些考虑因素将帮助您根据需要选择合适的工具。
编辑:协程现在有流程,一个非常类似于Rx的API。一个人可以比较每个人的利弊,但事实是差异很小。
协程作为核心是一种并发设计模式,带有附加库,其中一个是类似于Rx的流API。显然,协程的范围比Rx的范围要广得多,Coroutines可以做到很多事情Rx无法做到,而我无法一一列举。但是通常,如果我在一个项目中使用协程,则归结为以下原因之一:
我避免过多使用回调方式,这会损害可读性。协程使异步代码变得简单且易于编写。通过使用suspend关键字,您的代码看起来就像是同步代码。
我已经看到Rx在项目中主要用于替换回调的相同目的,但是如果您不打算修改您的体系结构以采用反应模式,Rx将是一个负担。考虑以下接口:
interface Foo {
fun bar(callback: Callback)
}
Coroutine等效项更加明确,带有返回类型和关键字suspend指示它是异步操作。
interface Foo {
suspend fun bar: Result
}
但是等效的Rx存在一个问题:
interface Foo {
fun bar: Single<Result>
}
在回调或Coroutine版本中调用bar()时,将触发计算;使用Rx版本,您可以表示可以随意触发的计算。您需要调用bar()然后订阅Single。通常这没什么大不了的,但是对于初学者来说有点令人困惑,并且可能导致细微的问题。
这些问题的一个例子,假设回调栏函数是这样实现的:
fun bar(callback: Callback) {
setCallback(callback)
refreshData()
}
如果未正确移植它,则将以只能触发一次的Single结尾,因为在bar()函数中而不是在订阅时调用refreshData()。初学者的错误是理所当然的,但问题是Rx不仅仅是回调替换,而且许多开发人员都在努力地掌握Rx。
如果您的目标是将异步任务从回调转换为更好的范例,那么协程非常适合,而Rx则增加了一些复杂性。
Kotlin协程与Rx不同。很难将它们之间进行比较,因为Kotlin协程是一种稀薄的语言功能(只有几个基本概念和一些基本功能可以操作它们),而Rx是一个相当繁重的库,具有各种各样的功能。现成的操作员。两者都是为了解决异步编程问题而设计的,但是它们的解决方案却大不相同:
Rx具有特殊的编程功能样式,几乎可以在任何编程语言中实现,而无需该语言本身的支持。当眼前的问题很容易分解为一系列标准运算符时,它会很好地工作,否则就不会那么好。
Kotlin协程提供了一种语言功能,可让库作者实现各种异步编程样式,包括但不限于功能反应式(Rx)。使用Kotlin协程,您还可以以命令式,基于承诺/未来的方式,基于参与者的方式等编写异步代码。
将Rx与基于Kotlin协程实现的某些特定库进行比较比较合适。
以kotlinx.coroutines库为例。该库提供了一组async/await
通常被烘焙到其他编程语言中的原语和渠道。它也支持轻量级的未来演员。您可以通过示例在kotlinx.coroutines指南中阅读更多内容。
kotlinx.coroutines
在某些用例中,所提供的通道可以替换或增加Rx。有单独的协程反应流指南,深入探讨了与Rx的异同。
try-catch
。您将获得开箱即用的范围控件,该控件清晰,直观地划分了您要保护的内容。您可以嵌套这些块并编写仍然很容易推理的复杂错误处理模式。从语法上讲,所有基于高阶函数的库都可以使用的方法链。协程有整个语言。
我非常了解RxJava,最近我改用Kotlin Coroutines和Flow。
RxKotlin与RxJava基本相同,只是添加了一些语法糖以使其更舒适/惯用Kotlin编写RxJava代码。
RxJava与Kotlin Coroutines之间的“公平”比较应该包括Flow在内,我将尝试在这里解释原因。这会有点长,但是我将尝试通过示例尽可能地简化它。
使用RxJava,您有不同的对象(从版本2开始):
// 0-n events without backpressure management
fun observeEventsA(): Observable<String>
// 0-n events with explicit backpressure management
fun observeEventsB(): Flowable<String>
// exactly 1 event
fun encrypt(original: String): Single<String>
// 0-1 events
fun cached(key: String): Maybe<MyData>
// just completes with no specific results
fun syncPending(): Completable
在kotlin协程+流中,您不需要太多实体,因为如果没有事件流,则可以只使用简单的协程(暂停功能):
// 0-n events, the backpressure is automatically taken care off
fun observeEvents(): Flow<String>
// exactly 1 event
suspend fun encrypt(original: String): String
// 0-1 events
suspend fun cached(key: String): MyData?
// just completes with no specific results
suspend fun syncPending()
奖励:Kotlin Flow /协程支持null
值(RxJava 2删除了支持)
随着RxJava你有这么多的运营商(map
,filter
,flatMap
,switchMap
,...),并且其中大部分有一个为每个实体类型的版本(Single.map()
,Observable.map()
,...)。
Kotlin Coroutines + Flow不需要那么多运算符,下面以最常见的运算符为例,说明原因
地图()
RxJava:
fun getPerson(id: String): Single<Person>
fun observePersons(): Observable<Person>
fun getPersonName(id: String): Single<String> {
return getPerson(id)
.map { it.firstName }
}
fun observePersonsNames(): Observable<String> {
return observePersons()
.map { it.firstName }
}
Kotlin协程+ Flow
suspend fun getPerson(id: String): Person
fun observePersons(): Flow<Person>
suspend fun getPersonName(id: String): String? {
return getPerson(id).firstName
}
fun observePersonsNames(): Flow<String> {
return observePersons()
.map { it.firstName }
}
对于“单个”情况,您不需要运算符,并且对于这种情况,它非常相似Flow
。
flatMap()
假设您需要每个人从数据库(或远程服务)中获取保险
RxJava的
fun fetchInsurance(insuranceId: String): Single<Insurance>
fun getPersonInsurance(id: String): Single<Insurance> {
return getPerson(id)
.flatMap { person ->
fetchInsurance(person.insuranceId)
}
}
fun obseverPersonsInsurances(): Observable<Insurance> {
return observePersons()
.flatMap { person ->
fetchInsurance(person.insuranceId) // this is a Single
.toObservable() // flatMap expect an Observable
}
}
让我们一起来看看Kotlin Coroutiens + Flow
suspend fun fetchInsurance(insuranceId: String): Insurance
suspend fun getPersonInsurance(id: String): Insurance {
val person = getPerson(id)
return fetchInsurance(person.insuranceId)
}
fun obseverPersonsInsurances(): Flow<Insurance> {
return observePersons()
.map { person ->
fetchInsurance(person.insuranceId)
}
}
像以前一样,在简单的协程情况下,我们不需要运算符,我们只需像使用异步函数那样编写代码,就使用暂停函数即可。
而且,Flow
由于这不是错字,所以不需要flatMap
运算符,我们可以使用map
。原因是map lambda是一个暂停函数!我们可以在其中执行暂挂代码!!!
为此,我们不需要其他运算符。
对于更复杂的内容,可以使用Flowtransform()
运算符。
每个Flow运算符都接受暂停功能!
因此,如果您需要,filter()
但过滤器需要执行网络通话,则可以!
fun observePersonsWithValidInsurance(): Flow<Person> {
return observerPersons()
.filter { person ->
val insurance = fetchInsurance(person.insuranceId)
insurance.isValid()
}
}
delay(),startWith(),concatWith(),...
在RxJava中,您有许多运算符可用于在延迟前后添加延迟或添加项目:
使用kotlin Flow,您可以轻松地:
grabMyFlow()
.onStart {
// delay by 3 seconds before starting
delay(3000L)
// just emitting an item first
emit("First item!")
emit(cachedItem()) // call another suspending function and emit the result
}
.onEach { value ->
// insert a delay of 1 second after a value only on some condition
if (value.length() > 5) {
delay(1000L)
}
}
.onCompletion {
val endingSequence: Flow<String> = grabEndingSequence()
emitAll(endingSequence)
}
错误处理
RxJava有很多运算符来处理错误:
使用Flow,您只需要运算符即可catch()
:
grabMyFlow()
.catch { error ->
// emit something from the flow
emit("We got an error: $error.message")
// then if we can recover from this error emit it
if (error is RecoverableError) {
// error.recover() here is supposed to return a Flow<> to recover
emitAll(error.recover())
} else {
// re-throw the error if we can't recover (aka = don't catch it)
throw error
}
}
并具有暂停功能,您可以使用try {} catch() {}
。
易于编写的流程运算符
由于协程为引擎下的Flow提供了动力,因此编写操作符更加容易。如果您曾经检查过RxJava运算符,就会看到它有多难,需要学习多少东西。
编写Kotlin Flow运算符比较容易,您只需在此处查看已经属于Flow的运算符的源代码即可了解一个想法。原因是协程使编写异步代码更容易,并且运算符使用起来更自然。
另外,Flow运算符都是kotlin扩展函数,这意味着您或库都可以轻松添加运算符,并且使用它们不会感到奇怪(例如observable.lift()
或observable.compose()
)。
上游线程不会向下游泄漏
这到底是什么意思?
让我们以这个RxJava示例为例:
urlsToCall()
.switchMap { url ->
if (url.scheme == "local") {
val data = grabFromMemory(url.path)
Flowable.just(data)
} else {
performNetworkCall(url)
.subscribeOn(Subscribers.io())
.toObservable()
}
}
.subscribe {
// in which thread is this call executed?
}
那么回调在哪里subscribe
执行?
答案是:
依靠...
如果来自网络,则它位于IO线程中;如果它来自另一个分支,则未定义,取决于使用哪个线程发送url。
这就是“上游线程向下游泄漏”的概念。
使用Flow和Coroutines并非如此,除非您明确要求此行为(使用Dispatchers.Unconfined
)。
suspend fun myFunction() {
// execute this coroutine body in the main thread
withContext(Dispatchers.Main) {
urlsToCall()
.conflate() // to achieve the effect of switchMap
.transform { url ->
if (url.scheme == "local") {
val data = grabFromMemory(url.path)
emit(data)
} else {
withContext(Dispatchers.IO) {
performNetworkCall(url)
}
}
}
.collect {
// this will always execute in the main thread
// because this is where we collect,
// inside withContext(Dispatchers.Main)
}
}
}
协程代码将在已被执行的上下文中运行。而且只有网络调用的部分将在IO线程上运行,而我们在此处看到的其他所有内容都将在主线程上运行。
好吧,实际上,我们不知道内部的代码grabFromMemory()
将在哪里运行,如果它是一个挂起函数,我们只知道它将在Main线程内被调用,但是在该挂起函数内部,我们可以使用另一个Dispatcher,但是何时使用返回结果,val data
它将再次出现在主线程中。
这意味着,看一段代码,如果看到一个显式的Dispatcher =就是那个调度程序,如果看不到它,那么就更容易知道它将在哪个线程中运行:在任何线程调度程序中,您正在查看的悬浮调用正在被呼叫。
这不是Kotlin发明的概念,但是他们比我所知道的任何其他语言都接受的更多。
那是什么
使用RxJava,您订阅了可观察Disposable
对象,它们为您提供了一个对象。
您需要在不再需要它时进行处理。因此,您通常要做的是保留对其的引用(或将其放在中CompositeDisposable
),以便以后dispose()
不再需要它时对其进行调用。如果您不这样做,则短绒棉会警告您。
RxJava比传统线程好一些。当您创建一个新线程并在其上执行某些操作时,这是“一劳永逸”的事情,甚至没有办法取消它:Thread.stop()
已过时,有害且最近的实现实际上不起作用。Thread.interrupt()
使您的线程失败等。所有异常都丢失了。
使用kotlin协程和流程,它们颠覆了“一次性”概念。没有不能创建协程CoroutineContext
。
此上下文定义了scope
协程的。在其中产生的每个子协程将共享相同的作用域。
如果您订阅流程,则必须位于协程内部或提供作用域。
您仍然可以参考您启动的协程(Job
)并取消它们。这将自动取消该协程的每个孩子。
如果您是Android开发人员,他们会自动为您提供这些范围。示例:viewModelScope
您可以在具有该范围的viewModel内启动协程,因为它们会在清除viewmodel时自动取消。
viewModelScope.launch {
// my coroutine here
}
如果某个子项失败,则某些作用域将终止;如果某个子项失败,则其他作用域将使每个子项退出自己的生命周期而不停止其他子项(SupervisedJob
)。
为什么这是一件好事?
让我试着像罗曼·伊里扎洛夫(Roman Elizarov)那样解释它。
一些旧的编程语言具有这种概念goto
,基本上可以让您随意从一行代码跳到另一行代码。
功能非常强大,但是如果滥用它,最终可能会导致难以理解的代码,难以调试和推理的结果。
因此,新的编程语言最终将其从语言中完全删除。
当您使用if
orwhile
或when
代码时更容易推理:无论这些块内发生了什么,您最终都会从它们中脱颖而出,这是一个“上下文”,您不会有奇怪的跳入跳出。
启动线程或订阅可观察到的RxJava与goto相似:您正在执行的代码将继续执行,直到“其他位置”停止为止。
对于协程,通过要求您提供上下文/范围,您可以知道,当您的范围超出范围时,协程将在上下文完成时完成,而您只有一个协程还是一万个协程都不重要。
您仍可以通过使用协程“转到”,GlobalScope
出于与您不应该goto
在提供协程的语言中使用的原因相同,不应该这样做。
Flow仍在开发中,并且Kotlin Coroutines Flow中目前尚不提供RxJava中的某些功能。
目前最大的失踪是share()
操作员及其朋友(publish()
,replay()
等等。。。)
它们实际上处于开发的高级状态,预计将很快发布(已经发布的kotlin之后不久1.4.0
),您可以在此处查看API设计:
您链接的演讲/文档不谈论频道。通道填补了您当前对协程的了解与事件驱动的编程之间的空白。
使用协程和通道,您可以像使用rx一样进行事件驱动的编程,但是您可以使用具有同步外观的代码来完成它,而无需使用许多“自定义”运算符。
如果您想更好地理解这一点,我建议您去看看Kotlin,因为这些概念更加成熟和完善(不是实验性的)。查看core.async
Clojure,Rich Hickey的视频,帖子和相关讨论。
协程旨在提供轻量级的异步编程框架。在启动异步作业所需的资源方面轻巧。协程不使用外部API强制执行,对于用户(程序员)来说更自然。相比之下,RxJava + RxKotlin具有Kotlin并不需要的其他数据处理程序包,该程序在标准库中具有非常丰富的API,用于序列和集合处理。
如果您想了解有关在Android上实际使用协程的更多信息,我可以推荐我的文章:https : //www.netguru.com/codestories/android-coroutines-%EF%B8%8Fin-2020