Kotlin协程如何比RxKotlin更好?

时间:2017-02-06 10:55:51

标签: kotlin rx-kotlin

为什么我要使用Kotlin协同程序?

似乎RxKotlin库更加多才多艺。 Kotlin协同程序看起来效率显着降低,相比之下使用起来更加麻烦。

我根据安德烈·布雷斯拉夫(JetBrains)的设计讲话对协程书发表意见:https://www.youtube.com/watch?v=4W3ruTWUhpw

可以在此处访问来自谈话的幻灯片:https://www.slideshare.net/abreslav/jvmls-2016-coroutines-in-kotlin

编辑(感谢@hotkey):

更好地了解协程的当前状态:https://github.com/Kotlin/KEEP/blob/master/proposals/coroutines.md

5 个答案:

答案 0 :(得分:69)

Kotlin协同程序与Rx不同。很难比较它们,因为Kotlin协程是一种瘦的语言特性(只有几个基本概念和一些基本功能来操纵它们),而Rx是一个非常重的库,有很多种类的即用型运营商。两者都旨在解决异步编程问题,但他们的解决方案却截然不同:

  • Rx具有特定的编程功能,几乎可以在任何编程语言中实现,而无需语言本身的支持。当手头的问题很容易分解成一系列标准操作符时,它很有效。否则就不太好了。

  • Kotlin协程提供了一种语言功能,允许库编写者实现各种异步编程风格,包括但不限于功能反应式(Rx)。使用Kotlin协程,您还可以以命令式,基于承诺/期货的方式,以演员风格等方式编写异步代码。

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

kotlinx.coroutines library为例。该库提供了一组原语,如async/await和通常被烘焙到其他编程语言中的通道。它还支持轻量级的未来演员。您可以在Guide to kotlinx.coroutines by example中阅读更多内容。

kotlinx.coroutines提供的频道可以在某些用例中替换或扩充Rx。有一个单独的Guide to reactive streams with coroutines与Rx有更深入的相似之处和不同之处。

答案 1 :(得分:66)

Rx有两个部分; Observable模式,以及一组操作,转换和组合它们的实体操作符。 Observable模式本身并没有做很多事情。与Coroutines相同;它只是另一种处理异步问题的范例。您可以比较回调,Observable和协同程序的优缺点来解决给定的问题,但您无法将范例与功能齐全的库进行比较。这就像将语言与框架进行比较一样。

Kotlin协程如何比RxKotlin更好?还没有使用协同程序,但它看起来类似于C#中的异步/等待。您只需编写顺序代码,一切都像编写同步代码一样简单...除了它异步执行。它更容易掌握。

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

修改:现在我正在使用协同程序,现在是时候进行更新了。

RxKotlin只是在Kotlin中使用RxJava的语法糖,所以我将在下面谈论RxJava而不是RxKotlin。协同程序是比RxJava更低的杠杆和更一般的概念,它们服务于其他用例。也就是说,有一个用例可以比较RxJava和coroutines(channel),它会异步传递数据。 Coroutines在这方面优于RxJava:

协同程序更好地处理资源

  • 在RxJava中,您可以将计算分配给调度程序,但subscribeOn()ObserveOn()令人困惑。每个协同程序都有一个线程上下文并返回到父上下文。对于一个渠道,双方(生产者,消费者)都在他自己的背景下执行。协同程序在线程或线程池修饰方面更直观。
  • 协同程序可以更好地控制这些计算何时发生。例如,您可以传递手(yield),优先排序(select),并行化(producer上的多个actor / channel)或锁定资源({{ 1}})用于给定的计算。它可能与服务器(RxJava首先出现)无关,但在资源有限的环境中,可能需要这种控制级别。
  • 由于它具有反应性,背压在RxJava中并不适合。在另一端Mutex到通道是一个暂停功能,在达到通道容量时暂停。它是由大自然给出的开箱即用的背压。你也可以send()进行通道,在这种情况下,呼叫永远不会暂停,但如果频道已满,则返回offer(),从RxJava有效地再现false。或者您可以编写自己的自定义背压逻辑,这对于协同程序来说并不困难,特别是与RxJava相同。

还有另一个用例,协同程序闪耀,这将回答你的第二个问题"为什么我要使用Kotlin协同程序?"。协同程序是后台线程或onBackpressureDrop()(Android)的完美替代品。它就像AsyncTask一样简单。当然,您也可以使用launch { someBlockingFunction() }Schedulers来使用RxJava实现此目的。你不会(或者很少)使用Observer模式和作为RxJava签名的运算符,暗示这项工作超出了RxJava的范围。 RxJava复杂性(这里的无用税)将使您的代码比Coroutine的版本更冗长,更简洁。

可读性至关重要。在这方面,RxJava和协同程序方法有很大不同。协同程序比RxJava更简单。如果您对Completablemap()以及功能性反应式编程感到不安,那么协同处理操作会更容易,涉及基础知识说明:flatmap()for,{{1}但是我个人觉得coroutine的代码更难理解非平凡的任务。特别是它涉及更多的嵌套和缩进,而RxJava中的操作符链则保持一致。功能样式编程使处理更加明确。最重要的是,RxJava可以通过其丰富的(OK,方式太丰富)运算符集合中的一些标准运算符来解决复杂的转换。当你有复杂的数据流需要大量的组合和转换时,RxJava会闪耀。

我希望这些考虑因素可以帮助您根据自己的需要选择合适的工具。

答案 2 :(得分:15)

我非常了解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协程+ 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协程+流量

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是一个暂停函数!我们可以在其中执行暂挂代码!!!

为此,我们不需要其他运算符。

对于更复杂的内容,可以使用Flow transform()运算符。

每个Flow运算符都接受暂停功能!

因此,如果您需要filter(),但您的过滤器需要执行网络通话,则可以!

fun observePersonsWithValidInsurance(): Flow<Person> {
  return observerPersons()
    .filter { person ->
        val insurance = fetchInsurance(person.insuranceId)
        insurance.isValid()
    }
}

delay(),startWith(),concatWith(),...

在RxJava中,您有许多运算符可用于在延迟前后添加延迟或添加项目:

  • delay()
  • delaySubscription()
  • startWith(T)
  • startWith(可观察)
  • 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运算符

由于协程为引擎内部的Flow提供了动力,因此编写操作符更加容易。如果您曾经检查过RxJava运算符,就会发现它有多难,需要学习多少东西。

编写Kotlin Flow运算符比较容易,只需查看已经属于Flow here的运算符的源代码,您便可以了解。原因是协程使编写异步代码更容易,并且操作符使用起来更自然。

作为奖励,Flow运算符都是kotlin扩展函数,这意味着您或库都可以轻松添加运算符,而且使用起来也不奇怪(例如observable.lift()observable.compose())。 / p>

上游线程不会向下游泄漏

这甚至意味着什么?

让我们以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线程中被调用,但是在该挂起函数中,我们可以拥有另一个正在使用分派器,但是当它返回结果val data时,它将再次出现在主线程中。

这意味着,查看一段代码,可以更轻松地知道它将在哪个线程中运行,如果您看到一个显式的Dispatcher =这是该调度程序,如果您没有看到它:在任何线程调度程序中,悬挂调用正在看着被呼叫。

结构化并发

这不是Kotlin发明的概念,但是他们比我所知道的任何其他语言都拥护更多的东西。

如果我在这里解释的内容不足以让您阅读this article或观看this video

那是什么?

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

功能非常强大,但是如果被滥用,您将可能会很难理解代码,难以调试并难以推理。

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

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

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

对于协程,通过要求您提供一个上下文/范围,您知道,当作用域位于整个范围内时,协程将在上下文完成时完成,而无论您是一个协程还是一万个协程都没关系。

您仍然可以通过使用GlobalScope来使用协程,而出于相同的原因,您不应该在提供它的语言中使用goto

有缺点吗?

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

目前最大的失踪者是share()个运营商及其朋友(publish()replay()等...)

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

答案 3 :(得分:4)

您链接的谈话/文档不谈论频道。通道是填补你当前对协同程序和事件驱动编程的理解之间的差距。

使用协同程序和通道,您可以执行事件驱动编程,因为您可能习惯使用rx,但是您可以使用具有同步代码的代码并且没有那么多&#34; custom&#34;运算符。

如果你想更好地理解这一点,我建议你去看看kotlin,那些概念更成熟和更精致(不是实验性的)。查看来自Clojure的core.async,Rich Hickey视频,帖子和相关讨论。

答案 4 :(得分:2)

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

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