特效和异步IO的详细信息

时间:2018-12-08 12:49:07

标签: scala scala-cats cats-effect

几天来,我一直将注意力集中在猫效应和IO上。而且我觉得我对这种效果有误解,或者只是错过了要点。

  1. 首先-如果IO可以取代Scala的Future,我们如何创建异步IO任务?使用IO.shift吗?使用IO.async吗? IO.delay是同步还是异步?我们可以使用像这样的Async[F].delay(...)这样的代码来执行通用异步任务吗?还是在我们使用unsafeToAsyncunsafeToFuture调用IO时发生异步?
  2. 猫效应中异步和并发的意义是什么?他们为什么分开?
  3. IO是绿色线程吗?如果是,为什么有猫效果的纤维物体?据我了解,光纤是绿色线程,但是文档声称我们可以将IO视为绿色线程。

我希望对其中的任何内容进行一些澄清,因为我未能理解这些内容上的cats-effect文档,并且互联网没有那么帮助...

1 个答案:

答案 0 :(得分:15)

  

如果IO可以取代Scala的Future,我们如何创建异步IO任务

首先,我们需要弄清什么是异步任务。通常 async 的意思是“不阻塞操作系统线程”,但是由于您提到的是Future,所以有点模糊。说,如果我写了:

Future { (1 to 1000000).foreach(println) }

它不是 async ,因为它是一个阻塞循环并阻塞了输出​​,但是它可能会在由隐式ExecutionContext管理的另一个OS线程上执行。等效的cats-effect代码为:

for {
  _ <- IO.shift
  _ <- IO.delay { (1 to 1000000).foreach(println) }
} yield ()

(不是较短的版本)

所以

  • IO.shift用于更改线程/线程池。 Future会在每次操作中执行此操作,但这并不是从性能角度考虑。
  • IO.delay {...}(又称IO { ... })不会使 NOT 异步,并且不会进行 NOT 切换线程。它用于从同步副作用API创建简单的IO

现在,让我们回到真正的异步。这里要理解的是:

每个异步计算都可以表示为带有回调的函数。

无论您使用的是返回Future还是Java的CompletableFuture或类似NIO CompletionHandler之类的API,都可以将其全部转换为回调。这就是IO.async的用途:您可以将任何接受回调的功能转换为IO。并以此类推:

for {
  _ <- IO.async { ... }
  _ <- IO(println("Done"))
} yield ()

Done仅在(如果)调用...中的计算时才打印。您可以将其视为阻塞绿色线程,而不是OS线程。

所以

  • IO.async用于将所有已经异步的计算转换为IO
  • IO.delay用于将任何完全同步计算转换为IO
  • 具有真正异步计算的代码的行为就像阻塞了一个绿色线程。

使用Future时,最接近的类比是创建一个scala.concurrent.Promise并返回p.future


  

当我们使用unsafeToAsync或unsafeToFuture调用IO时,还是发生异步?

排序。使用IO,除非您调用其中之一(或使用IOApp),否则什么都不会发生。但是IO不能保证您将在不同的OS线程上执行,甚至不能异步执行,除非您使用IO.shiftIO.async明确要求这样做。

例如,您可以保证随时进行线程切换。 (IO.shift *> myIO).unsafeRunAsyncAndForget()。这完全有可能是因为myIO直到被问到,无论您是以val myIO还是def myIO的身份执行。

但是,您不能神奇地将阻塞操作转换为非阻塞操作。 FutureIO都不可能。


  

猫效应中异步和并发的意义是什么?他们为什么分开?

AsyncConcurrent(和Sync)是类型类。它们的设计使程序员可以避免被锁定在cats.effect.IO上,并且可以为您提供支持您选择的任何内容的API,例如monix Task或Scalaz 8 ZIO,甚至是monad转换器类型,例如OptionT[Task, *something*]。 fs2,monix和http4s之类的库利用它们来为您提供更多使用它们的选择。

ConcurrentAsync之上添加了额外的内容,其中最重要的是.cancelable.start。它们与Future没有直接的类比,因为它根本不支持取消。

.cancelable.async的一个版本,它使您还可以指定一些逻辑来取消要包装的操作。一个常见的示例是网络请求-如果您不再对结果感兴趣,则可以中止它们而不必等待服务器响应,也不会浪费任何套接字或处理时间来读取响应。您可能永远都不会直接使用它,但是它有它的位置。

但是如果您不能取消可取消的操作,有什么好处呢?这里的主要观察结果是您不能从内部取消操作。其他人必须做出该决定,并且这将同时发生与操作本身(这是类型类获得名称的地方)。这就是.start出现的地方。简而言之,

.start是绿色线程的显式分支。

执行someIO.start与执行val t = new Thread(someRunnable); t.start()相似,但现在是绿色。 Fiber本质上是Thread API的简化版本:您可以执行.join,就像Thread#join()一样,但是它不会阻塞OS线程;和.cancel,它是.interrupt()的安全版本。


请注意,还有其他分叉绿色线程的方法。例如,执行并行操作:

val ids: List[Int] = List.range(1, 1000)
def processId(id: Int): IO[Unit] = ???
val processAll: IO[Unit] = ids.parTraverse_(processId)

将把所有ID分叉到绿色线程,然后将它们全部加入。或使用.race

val fetchFromS3: IO[String] = ???
val fetchFromOtherNode: IO[String] = ???

val fetchWhateverIsFaster = IO.race(fetchFromS3, fetchFromOtherNode).map(_.merge)

将并行执行提取,使您的第一个结果完成,并自动取消较慢的提取。因此,执行.start和使用Fiber并不是派生更多绿色线程的唯一方法,而是最明确的方法。答案是:

  

IO是绿色线程吗?如果是,为什么有猫效果的纤维物体?据我了解,光纤是绿色线程,但是文档声称我们可以将IO视为绿色线程。

  • IO就像一个绿色线程,这意味着您可以让许多它们并行运行而没有OS线程的开销,并且理解代码的行为就像是阻塞了结果计算。

  • Fiber是用于控制显式分叉的绿色线程的工具(等待完成或取消)。