如果 IO 可以替代 Scala 的 Future,我们如何创建异步 IO 任务
首先,我们需要澄清什么是异步任务。通常async的意思是“不阻塞操作系统线程”,但是既然你提到Future
了,那就有点模糊了。说,如果我写:
Future { (1 to 1000000).foreach(println) }
它不会是async,因为它是一个阻塞循环和阻塞输出,但它可能会在不同的操作系统线程上执行,由隐式 ExecutionContext 管理。等效的猫效应代码将是:
for {
_ <- IO.shift
_ <- IO.delay { (1 to 1000000).foreach(println) }
} yield ()
(这不是较短的版本)
所以,
IO.shift
用于可能更改线程/线程池。Future
每次操作都会这样做,但在性能方面它不是免费的。
IO.delay
{ ... } (aka IO { ... }
) 不会进行任何异步操作,也不会切换线程。它用于IO
从同步的副作用 API 创建简单的值
现在,让我们回到真正的 async。这里要理解的是:
每个异步计算都可以表示为一个接受回调的函数。
无论您使用的是返回的 APIFuture
还是 Java 的CompletableFuture
,还是类似 NIO 的东西CompletionHandler
,都可以转换为回调。这就是IO.async
目的:您可以将任何接受回调的函数转换为IO
. 如果是这样:
for {
_ <- IO.async { ... }
_ <- IO(println("Done"))
} yield ()
Done
仅当(以及如果)回调中的计算时才会打印...
。您可以将其视为阻塞绿色线程,而不是 OS 线程。
所以,
IO.async
用于将任何已经异步的计算转换为IO
.
IO.delay
用于将任何完全同步的计算转换为IO
.
- 具有真正异步计算的代码的行为就像它阻塞了一个绿色线程。
使用 s 时最接近的类比Future
是创建 ascala.concurrent.Promise
和返回p.future
。
或者当我们使用 unsafeToAsync 或 unsafeToFuture 调用 IO 时会发生异步?
有点。使用IO
,除非您调用其中之一(或使用) ,否则不会发生任何事情。IOApp
但是 IO 不保证您将在不同的操作系统线程上执行,甚至异步执行,除非您使用IO.shift
or明确要求IO.async
。
您可以随时使用 eg 保证线程切换(IO.shift *> myIO).unsafeRunAsyncAndForget()
。这正是可能的,因为myIO
在被要求之前不会执行,无论您将它作为val myIO
还是def myIO
.
但是,您不能神奇地将阻塞操作转换为非阻塞操作。Future
使用或使用都不可能IO
。
猫效应中的异步和并发有什么意义?他们为什么分开?
Async
和Concurrent
(and Sync
) 是类型类。它们的设计使程序员可以避免被锁定cats.effect.IO
并可以为您提供支持您选择的任何内容的 API,例如 monix Task 或 Scalaz 8 ZIO,甚至是OptionT[Task, *something*]
. fs2、monix 和 http4s 等库利用它们为您提供更多使用它们的选择。
Concurrent
在 之上添加额外的东西Async
,其中最重要的是.cancelable
and .start
。这些与 没有直接的类比Future
,因为它根本不支持取消。
.cancelable
是一个版本,.async
它允许您还指定一些逻辑来取消您正在包装的操作。一个常见的例子是网络请求——如果你不再对结果感兴趣,你可以在不等待服务器响应的情况下中止它们,并且不要浪费任何套接字或处理时间来读取响应。你可能永远不会直接使用它,但它有它的位置。
但是如果你不能取消它们,那么可取消的操作有什么用呢?这里的主要观察是您不能从其内部取消操作。其他人必须做出这个决定,这将与操作本身同时发生(这是类型类的名称)。这就是.start
进来的地方。简而言之,
.start
是一个绿色线程的显式分支。
做事someIO.start
类似于做事val t = new Thread(someRunnable); t.start()
,只是现在是绿色的。并且本质上是APIFiber
的精简版本:你可以做,就像,但它不会阻塞 OS 线程;和,这是 的安全版本。Thread
.join
Thread#join()
.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 是绿色线程吗?如果是,为什么猫效应中有一个 Fiber 对象?据我了解,Fiber 是绿色线程,但文档声称我们可以将 IO 视为绿色线程。