几天来,我一直将注意力集中在猫效应和IO上。而且我觉得我对这种效果有误解,或者只是错过了要点。
IO.shift
吗?使用IO.async
吗? IO.delay
是同步还是异步?我们可以使用像这样的Async[F].delay(...)
这样的代码来执行通用异步任务吗?还是在我们使用unsafeToAsync
或unsafeToFuture
调用IO时发生异步?我希望对其中的任何内容进行一些澄清,因为我未能理解这些内容上的cats-effect文档,并且互联网没有那么帮助...
答案 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.shift
或IO.async
明确要求这样做。
例如,您可以保证随时进行线程切换。 (IO.shift *> myIO).unsafeRunAsyncAndForget()
。这完全有可能是因为myIO
直到被问到,无论您是以val myIO
还是def myIO
的身份执行。
但是,您不能神奇地将阻塞操作转换为非阻塞操作。 Future
和IO
都不可能。
猫效应中异步和并发的意义是什么?他们为什么分开?
Async
和Concurrent
(和Sync
)是类型类。它们的设计使程序员可以避免被锁定在cats.effect.IO
上,并且可以为您提供支持您选择的任何内容的API,例如monix Task或Scalaz 8 ZIO,甚至是monad转换器类型,例如OptionT[Task, *something*]
。 fs2,monix和http4s之类的库利用它们来为您提供更多使用它们的选择。
Concurrent
在Async
之上添加了额外的内容,其中最重要的是.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
是用于控制显式分叉的绿色线程的工具(等待完成或取消)。