Kotlin协程如何比RxKotlin更好?


Answers:


99

免责声明:由于Coroutines现在具有流程API,与Rx one非常相似,因此该答案的某些部分无关紧要。如果您想要最新的答案,请跳至上一个编辑。

Rx中有两个部分:可观察模式,以及一组可靠的运算符来进行操作,转换和组合。可观察模式本身并没有做什么用。与协程相同;这只是处理异步的另一范式。您可以比较回调,Observable和协程的优点/缺点来解决给定的问题,但是不能将范例与功能齐全的库进行比较。这就像将语言与框架进行比较。

Kotlin协程如何比RxKotlin更好?尚未使用协程,但它看起来类似于C#中的异步/等待。您只需编写顺序代码,除异步执行外,其他一切都与编写同步代码一样容易。更容易掌握。

为什么我要使用Kotlin协程?我会自己回答。大多数时候,我会坚持使用Rx,因为我更喜欢事件驱动的体系结构。但是在出现编写顺序代码的情况时,需要在中间调用异步方法,我会很乐意利用协程保持这种方式,并避免将所有内容包装在Observable中。

编辑:现在,我正在使用协程,是时候进行更新了。

RxKotlin只是在Kotlin中使用RxJava的语法糖,因此下面我将谈论RxJava而不是RxKotlin。与RxJava相比,协程是一个较低的杠杆,是更通用的概念,它们可用于其他用例。也就是说,在一个用例中,您可以比较RxJava和协程(channel),它异步地传递数据。协程在这里比RxJava有明显的优势:

协程更好地处理资源

  • 在RxJava你可以分配计算以调度,但subscribeOn()ObserveOn()混淆了。每个协程都有一个线程上下文并返回到父上下文。对于渠道,双方(生产者,消费者)都在自己的上下文中执行。协程在线程或线程池影响方面更为直观。
  • 协程可以更好地控制这些计算的发生时间。例如yield,对于给定的计算,您可以传递hand(),priorize (),priorize(select),parallelize(multiple producer/ actoron channel)或锁定资源(Mutex)。在服务器上(RxJava排在第一位)可能并不重要,但是在资源有限的环境中,可能需要此级别的控制。
  • 由于它具有反应性,所以背压不适用于RxJava。在send()通道的另一端,是一个暂停功能,当达到通道容量时会暂停。这是自然产生的开箱即用的背压。您也可以offer()进行频道设置,在这种情况下,调用不会挂起,而false在频道已满的情况下返回,可以有效地onBackpressureDrop()从RxJava复制。或者,您也可以编写自己的自定义背压逻辑,这对于协程并不难,尤其是与RxJava相比。

还有另一个用例,协程会发光,这将回答您的第二个问题“我为什么要使用Kotlin协程?”。协程是后台线程或AsyncTask(Android)的完美替代。就像一样简单launch { someBlockingFunction() }。当然,您也可以使用RxJavaSchedulers并使用来实现此目的Completable。您不会(或很少)使用Observer模式和作为RxJava签名的运算符,这表明这项工作超出RxJava的范围。RxJava的复杂性(此处无用的税)将使您的代码比Coroutine的版本更冗长,更简洁。

可读性很重要。在这方面,RxJava和协程的方法相差很多。协程比RxJava简单。如果你不放心用map()flatmap()和功能反应式编程一般,协程的操作更容易,涉及到基本的指令:foriftry/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则增加了一些复杂性。


3
因此,您无需将所有内容包装在Observable中,而是将所有内容包装在Future中。
Ruslan

13
幸运的是,Kotlin协程与C#和JS完全不同,不需要在Future中包装代码。您可以将期货与Kotlin协程一起使用,但是基于Kotlin协程的惯用代码几乎根本不使用任何期货。
Roman Elizarov

1
一个人可以很容易地使用通道来做事件驱动的架构。
pablisco

1
我个人出于一致性和复杂性的原因,会避免混合使用Coroutines和RxJava。根据您的用例,您可以考虑将协程与LiveData或新引入的类型一起使用Flow:Roman Elizarov:冷流,热通道
Geoffrey Marizy

3
此外,Coroutin也提供'''map()'''或'''flatMap()'''。Coroutine的“流”的作用与Rx中的Observable类似,您也可以为此使用很多运算符。另外,协程比Rx快得多,并且比Rx使用更少的资源。让我展示这篇文章。link.medium.com/o1QNGL2bvZ
MJ Studio

84

Kotlin协程与Rx不同。很难将它们之间进行比较,因为Kotlin协程是一种稀薄的语言功能(只有几个基本概念和一些基本功能可以操作它们),而Rx是一个相当繁重的库,具有各种各样的功能。现成的操作员。两者都是为了解决异步编程问题而设计的,但是它们的解决方案却大不相同:

  • Rx具有特殊的编程功能样式,几乎可以在任何编程语言中实现,而无需该语言本身的支持。当眼前的问题很容易分解为一系列标准运算符时,它会很好地工作,否则就不会那么好。

  • Kotlin协程提供了一种语言功能,可让库作者实现各种异步编程样式,包括但不限于功能反应式(Rx)。使用Kotlin协程,您还可以以命令式,基于承诺/未来的方式,基于参与者的方式等编写异步代码。

将Rx与基于Kotlin协程实现的某些特定库进行比较比较合适。

kotlinx.coroutines库为例。该库提供了一组async/await通常被烘焙到其他编程语言中的原语和渠道。它也支持轻量级的未来演员。您可以通过示例在kotlinx.coroutines指南中阅读更多内容。

kotlinx.coroutines在某些用例中,所提供的通道可以替换或增加Rx。有单独的协程反应流指南,深入探讨了与Rx的异同。


关于错误/异常处理管理呢?Rx比协程更好吗?
Piyush Katariya

2
如果将Rx与kotlinx.coroutines库进行比较,则它们都提供大致相同的错误/异常处理能力,以样式差异为模。您可以安装全局错误/异常处理程序,也可以使用各种结构在本地处理错误。
Roman Elizarov '18

2
我想说的是,协程在错误处理方面绝对更灵活,因为您可以使用老式的try-catch。您将获得开箱即用的范围控件,该控件清晰,直观地划分了您要保护的内容。您可以嵌套这些块并编写仍然很容易推理的复杂错误处理模式。从语法上讲,所有基于高阶函数的库都可以使用的方法链。协程有整个语言。
Marko Topolnik '18

不能“轻松分解为一系列标准运算符”的问题/代码的例子是什么?
yiati

嘿,罗马人对科特林领导层表示祝贺:)我对这个问题的回答有什么不对吗?
Daniele Segato

16

我非常了解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你有这么多的运营商(mapfilterflatMapswitchMap,...),并且其中大部分有一个为每个实体类型的版本(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中,您有许多运算符可用于在延迟前后添加延迟或添加项目:

  • 延迟()
  • delaySubscription()
  • startWith(T)
  • startWith(Observable)
  • concatWith(...)

使用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有很多运算符来处理错误:

  • onErrorResumeWith()
  • onErrorReturn()
  • onErrorComplete()

使用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,基本上可以让您随意从一行代码跳到另一行代码。

功能非常强大,但是如果滥用它,最终可能会导致难以理解的代码,难以调试和推理的结果。

因此,新的编程语言最终将其从语言中完全删除。

当您使用iforwhilewhen代码时更容易推理:无论这些块内发生了什么,您最终都会从它们中脱颖而出,这是一个“上下文”,您不会有奇怪的跳入跳出。

启动线程或订阅可观察到的RxJava与goto相似:您正在执行的代码将继续执行,直到“其他位置”停止为止。

对于协程,通过要求您提供上下文/范围,您可以知道,当您的范围超出范围时,协程将在上下文完成时完成,而您只有一个协程还是一万个协程都不重要。

您仍可以通过使用协程“转到”,GlobalScope出于与您不应该goto在提供协程的语言中使用的原因相同,不应该这样做。

有任何缺点吗?

Flow仍在开发中,并且Kotlin Coroutines Flow中目前尚不提供RxJava中的某些功能。

目前最大的失踪是share()操作员及其朋友(publish()replay()等等。。。)

它们实际上处于开发的高级状态,预计将很快发布(已经发布的kotlin之后不久1.4.0),您可以在此处查看API设计:


很棒的比较和总结。
Rvb84 '20

谢谢,我实际上是通过解决一些格式错误并添加了一些我忘记包括的部分来改善了它
Daniele Segato

6

您链接的演讲/文档不谈论频道。通道填补了您当前对协程的了解与事件驱动的编程之间的空白。

使用协程和通道,您可以像使用rx一样进行事件驱动的编程,但是您可以使用具有同步外观的代码来完成它,而无需使用许多“自定义”运算符。

如果您想更好地理解这一点,我建议您去看看Kotlin,因为这些概念更加成熟和完善(不是实验性的)。查看core.asyncClojure,Rich Hickey的视频,帖子和相关讨论。


3

协程旨在提供轻量级的异步编程框架。在启动异步作业所需的资源方面轻巧。协程不使用外部API强制执行,对于用户(程序员)来说更自然。相比之下,RxJava + RxKotlin具有Kotlin并不需要的其他数据处理程序包,该程序在标准库中具有非常丰富的API,用于序列和集合处理。

如果您想了解有关在Android上实际使用协程的更多信息,我可以推荐我的文章:https : //www.netguru.com/codestories/android-coroutines-%EF%B8%8Fin-2020

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.