STM

STM

软件事务性存储(STM)是一种允许任意组合原子操作的技术。它是数据库系统中事务管理的类似物。 STM可以原子方式执行多组内存操作。该 API 受到 Haskell 的 STM 库 启发,尽管 ZIO 中的实现完全不同。 与传统的低级锁定机制相比,STM具有许多优点: 组合性 避免死锁 异常或超时的时候自动回滚 避免由并发和锁粒度带来的压力 传统的使用锁的并发程序的主要问题在于难以实现组合,各自正确的代码片段组合在一起时可能会失败,因为当越来越多的锁被引入代码时,获取和释放锁的顺序不当很容易导致死锁。 事务性存储消除了许多基于底层锁的低级编程带来的困难,因为事务性数据结构是无锁的。 事务性数据结构 STM事务中可以有多种事务性数据结构: TRef: 一个对不可变量的可变引用 TPromise: 一个只能设置一次的可变引用 TArray: 一个可变引用的数组 TMap: 一个可变的 map TQueue: 一个可变的队列 TSet: 一个可变的集合 TSemaphore: 信号量 TReentrantLock: 重入锁 由于STM非常重视组合性,因此您可以在这些数据结构上建立并定义自己的并发数据结构。例如,您可以使用 TRef,TMap 和 TQueue 构建事务优先级队列。 STM 数据类型 STM[E, A] 表示可以以事务性的方式执行,并得到失败 E 或成功 A 的 effect。它 有一种更强大的变体 ZSTM[R, E, A],它支持环境类型 R,就如同 ZIO[R, E, A]。 STM(和它的变体ZSTM)数据类型不如 ZIO[R, E, A] 数据类型强大,这是因为它不允许您执行任意的 effect。因为 STM 内的动作可以执行任意次数(也可以回滚),所以在内存性事务中只能执行 STM 操作和纯的计算。STM 操作也不能在事务之外执行,因此您不能在 STM.atomical 的保护范围之外意外地读写事务数据结构(或不明确地提交事务)。例如: transferMoney 描述了发送方和接收方之间的原子事务过程。如果发件人帐户中没有足够的资金,交易将失败。这意味着个人帐户在自动完成借贷交易过程中,如果事务在中间失败,则整个过程将被回滚,并且看起来什么也没有发生过一样。在这里,我们看到 STM effect 通过 for-comprehension 按顺序来组合,并且通过 STM.atomical(或在任何单个 STM effect上调用 commit)将 STM effect 转换为可以执行的 ZIO effect。通过使用 STM.atomically(或 commit),程序员可以将 STM.atomically 中的各个操作视为不可分割,从而将它整体上看待成一个原子事务。 错误 STM 就像 ZIO 一样通过错误通道来支持错误输出。在 transferMoney 中,我们看到了一个产生错误的示例(STM.fail)。 STM 中的错误具有中止的语义:如果原子事务遇到错误,则该事务将回滚并且无效。 重试 STM.retry 是阻塞状态下的事务实现组合的关键。例如,如果我们要等待汇款人的账户有足够的钱时进行汇款(而不是立即失败),我们就可以用 STM.retry 来代替: STM.retry 将重试整个事务,直到成功为止(而不是像前面的示例一样失败)。但是请注意,仅当其下的事务数据结构发生改变时,重试才会开始。STM.retry 组合器还有许多其他变体,例如 STM.check。您可以用 STM.check(senderBal < amount) 替换,而不是 if (senderBal < amount) STM.retry else STM.unit。 选项性组合 STM 事务一般按顺序组合在一起,依次执行两个STM effect。然而,STM 事务也可以通过 orTry 选项性地组合在一起,只要其中一个执行通过即可。假设我们有两个 STM effect sA 和 sB,则我们可以以 sA orTry sB 的形式组合这两个 effect。事务将首先尝试运行 sA,如果无效需要重试,则 sA 被放弃,转而运行 sB,现在,如果 sB 也需要重试,则尝试重试整个过程,但是在这之前,它会等待被 sA 或 sB 调用的事务数据结构发生变换。使用 orTry 是一种优雅的技术,可用于确定 STM 事务是否需要阻塞。例如,我们可以用 orTry 来将(retry 版本的) transferMoneyNoMatterWhat 转变为会立刻失败的 STM 事务,如果发件人没有足够的钱,则该交易将立即失败,而无需重试: 因为 orTry 的语义,事务将因为没有钱而立即失败。

FiberRef

FiberRef[A] 表示对一个 A 类型值的可变引用建模。它有两个基本操作,set:将引用设置为新值;get:取回的当前值。 与 Ref[A] 不同,FiberRef[A] 中的值只会与当前执行中的纤程绑定。拥有同一个 FiberRef[A] 的不同纤程可以独立设置和读取参考值,而不会发生冲突。 您可以将 FiberRef 视为与 Java 的 ThreadLocal 类似。 操作 FiberRef[A] 具有几乎与 Ref[A] 相同的 API。它包括一些熟知的方法,例如: FiberRef#get:返回当前引用的值。 FiberRef#set:设置当前引用的值。 FiberRef#update/FiberRef#updateSome:用指定的函数更新引用值。 FiberRef#modify/FiberRef#modifySome:用指定的函数更新引用值,并允许该函数返回一个值。 您也可以用 locally 让 FiberRef 值的访问范围仅限定于给定的 effect: 传递 FiberRef[A] 在 ZIO#fork 指令中具有 copy-on-fork 的语意。 简而言之这意味着子纤程一开始就具有父纤程在 FiberRef 中的值。当子纤程改变了该值,这个变化只能被子纤程自己看到,父纤程还将保持自己的值。 您可以使用 Fiber#inheritRefs 方法从一个纤程中继承所有 FiberRef 的值: 请注意,join 会自动调用 inheritRefs。这实际上意味着以下两个效果的行为相同: 此外,您可以自定义如何(如果有的话)在建立分支纤程时更新值以及在合并纤程时如何合并值。为此,请在 FiberRef#make 期间指定所需的行为: 内存安全 FiberRef 中的值在其宿主纤程结束后,会被自动垃圾收集。无法访问的FiberRef(在用户代码中没有对其的引用)中的所有特定纤程的值也会被自动垃圾回收,哪怕它们曾经在当前运行的纤程中被使用过。

Fiber(纤程)

要在不影响当前进程的情况下执行 effect,可以使用纤程,这是一种轻量级的并发机制。 您可以通过 fork 让任何 IO[E, A] 立即产生出一个纤程 (UIO[Fiber[E, A]])。可以通过 join 来合并一个持有的纤程,该调用将会得到该纤程的返回值,或者也可以中断(interrupt)该纤程的执行,一个终止的纤程会安全地释放该纤程持有的所有资源。 在 JVM 上,纤程会使用到线程,但不会无限制地消耗线程。相反,纤程(对有限的线程以)高竞态地方式协同运行。 直到纤程已完成或已被彻底中断并且其所有终结器都已运行,中断操作才会返回。这些精确的语义是为了允许构建一个不会泄漏资源的程序。 fork0 是一个更强大的 fork 变种,它允许指定监管程序,该监管程序可以处理任何来自受监管的纤程的任何的不可恢复的错误,包括终结器中发生的所有此类错误。如果未指定监管程序,则将采用父纤程的监管程序,并以此递归直到根处理程序为止,监管程序可以在运行时(runtime)中指定(缺省的监管程序仅打印堆栈跟踪)。 错误模型 IO 的错误模型简单且一致,支持类型错误和终止,并且不违背 Functor 层次结构中的任何法则。 一个 IO[E, A] 的值只会引发E类型的错误。这个错误可以通过 either方法来恢复。这是一个不会失败的 effect,因为失败值被作为 Either 的成功值的一部分返回。 除了类型 E 错误外,一个纤程可能由于以下原因终止: 纤程自行终止或被另一个纤程中断。 “主”纤程不能被中断,因为它不是从任何其他纤程中分支出来的。 纤程无法处理某些 E 型错误。只有在 IO.fail 未被处理时才会发生。对于 UIO[A] 类型的值,这种类型的故障是不可能的。 纤程中的缺陷会导致不可恢复的错误。但是只能通过两种方式产生这种缺陷: 将一个偏函数传递给高阶函数,比如 map 或 flatMap。例如,io.map(_ => throw e) 或 io.flatMap(a => throw e)。解决此问题的方法是不要将不纯函数传递给像 ZIO 这样的纯函数库,因为这样做会导致违反代数定律和破坏方程式推理。 在 IO.effectTotal 等函数中使用会抛出错误的代码。要在 IO 中使用不完全效果的代码,正确的解决办法是使用诸如 IO.effect 之类的方法,该方法可以将异常安全地转换为值。 当纤程被终止时,终止原因被以 Throwable 的方式传递给纤程的监管程序,该管理程序可以选择记录日志,打印堆栈跟踪记录,重新启动纤程或执行其它符合上下文的其他操作。 纤程如果被中断,其自身无法停止这个过程。哪怕在中断过程中某些终结器抛出了不可恢复的错误,所有终结器也都依然会被执行。终结器抛出的错误将被传递给纤程的监管器。 在任何情况下,都不会丢失任何错误,这使得 IO 错误模型比 Scala 和 Java 中的try/catch/finally 结构更易于诊断,因为它们很容易丢失错误。 并行 zipPar 可以用于并行计算: zipPar 组合器具有资源安全的语义。如果一个计算失败,另一计算将被中断,以防止浪费资源。 Racing 两个 IO 操作可以执行“竞速”运算,这意味着它们将并行执行,并且将返回先成功的运算的值。 race 组合器也是资源安全的,这意味着如果两个操作之一返回一个值,则另一个将被中断,以防止浪费资源。 race 和 zipPar 组合器是功能更强大的 raceWith 组合器的特例,它可以在两个操作中的第一个成功执行时执行用户定义的逻辑。 线程切换 – JVM 默认情况下,纤程不保证它们会在哪个线程上执行。它们可能会在线程之间切换,尤其是长时间执行时。 纤程只会在运行时(Runtime)系统的线程池中的线程间转移,这意味着默认情况下,长时间运行的纤程最终将回到运行时系统的线程池中,哪怕它们是从其他线程(池)中启动的,(异步)恢复后也是如此。 出于性能方面的考虑,纤程会尝试在同一个线程上执行一个(可配置的)最短的时间,然后切换到其它纤程。从异步回调中恢复的纤程将在它的启动线程上恢复,并持续一段时间,然后被切换到运行时线程池上恢复运行。 这样的默认配置有助于确保堆栈安全和协作式多任务处理。如果不需要自动线程转移,则可以在运行时(Runtime)中更改它们。

摘要

ZIO 包含以下数据类型,可以帮助您解决异步和并发编程中的复杂问题。 Fiber(纤程) – 纤程是对一个处于运行中的 IO 值的建模,它是一个绿色线程。 STM(Software Transactional Memory) – 软件事务性内存是一种支持事务运算并得到失败或成功的 effect。 ZIO – ZIO 是一个“效果化”的程序的模型,该程序可能失败或成功。 Managed – Managed 是一个值,它描述了一种可释放的,在给定范围内只能被消费一次的资源。 Promise – Promise 是一个变量模型,它可以被多个纤程共享但是只可以被设置一次。 Queue – Queue 是一个永不阻塞的异步队列,对于多个并发的生产者和使用者来说是安全的。 Ref – Ref[A] 对 A 类型的值的可变引用进行建模。它有两个基本的操作,set 将新值填充到 Ref 中,get 取回其当前内容。 Ref 上的所有操作都是原子和线程安全的,它为同步并发程序提供了可靠的基础。 FiberRef – FiberRef[A] 对 A 类型的值的可变引用进行建模。但与 Ref[A] 不同的是,该值仅能与当前执行中的 Fiber 绑定。您可以将其视为类似于 Java 的 ThreadLocal。 Schedule – Schedule 是一个可重复任务的模型,它可被用于重复执行成功的或重试失败的 IO 值。 Semaphore – 信号量 Semaphore  是一个可与 ZIO 中断配合使用的异步(非阻塞)信号量。 Chunk – ZIO Chunk: 一个高效的,纯的 Arrays 的替代方案。 TArray – TArray[A] 是可以参与事务运算的的可变数组。 TMap – A TMap[A] 是可以参与事务运算的的可变 map TPriorityQueue – A TPriorityQueue[A] 是可以参与事务的可变优先级队列。 TPromise – A TPromise 可参与事务运算的,只可以被设置一次的 Promise 可变量。 TQueue – A TQueue 是可以参与事务运算的可变队列。 TRef – A TRef 是一个可以参与事务运算的,对不可变量的可变参考。 TSet – A TSet 是可以参与事务的可变集合。 Has –  Has 用于表示一个 effect 对 A 类型服务的依赖关系。 ZLayer – A ZLayer 用于描述应用程序的一个“层”。 除了这些核心数据类型外,还可以在 ZIO streams 库中找到以下数据类型: Sink — Sink 是 Stream 中的数据消费者的,当它从 Stream 中消费得数据后可能会返回一个值。 Stream — Stream 是一个惰性的并发异步数据源。 要了解有关这些数据类型的更多信息,请浏览上面的页面,或查看 Scaladoc 文档。

平台

ZIO在最大程度上提供了跨平台的一致接口,从而允许开发人员编写一次代码并将其部署到任何地方。但是,要注意的平台之间存在一些不可避免的差异。 JVM ZIO支持 Java 版本 8 和更高版本以及 Scala 版本 2.11、2.12、2.13 和 Dotty。 在JVM上,Blocking 服务可用于将 effect 锁定在阻塞线程池,它已经包含在 ZEnv中。有关阻塞同步副作用的进一步讨论,请参见关于 创建 Effects 的文档。 Scala.js ZIO 支持 Scala.js 1.0. 尽管 ZIO 是零依赖的库,但这是建立在假设平台具有一些基本功能的基础上。特别是,由于 Scala.js 中缺少某些 java.time 方法的实现,因此用户必须自定义自己的java.time 依赖关系。 ZIO在其自己的内部测试套件中使用的是 scala-java-time。可以将其添加为依赖项,如下所示: 由于其单线程执行模型,Scala.js 不支持阻塞操作。因此,Blocking 服务不可用,也不包含在 ZEnv 中。另外,有一些方法 Scala.js 或者不支持,或者它们是不安全: 不支持 Console 服务中的 readLine 方法,因为 Scala.js 没有使用 Scala 标准库实现从控制台上阻塞读取一行输入和其下的方法。 运行时上的 unsafeRun,unsafeRunTask 和 unsafeRunSync 方法是不安全。所有这些方法均会同步将值返回并且可能阻塞,如果 effect 中包含异步步骤,包括运行时为保证执行公平性导入的摆出点,用户应改用unsafeRunAsync,unsafeRunAsync_ 或 unsafeRunToFuture方法。 Scala Native 目前对 Scala Native 的支持尚处于试验阶段。当支持 Scala Native 平台时,将添加更多详细信息。

性能

zio 拥有出色的性能,具有手动优化的低级解释器,该解释器可实现对右相关联的绑定的零分配,和最小化的左相关联的绑定的分配。 Monix Task 和 Cats IO:benchmarks 项目可用于将 IO(特指 ZIO) 与其它 Monad effect ,包括“Future”(虽然它不是Monad effect,但可供参考),Monix Task 和 Cats IO 进行比较。 截至撰写本文时,IO显著地快于或至少与所有其他纯函数解决方案可相提并论。

背景知识

过程式程序中使用到的过程函数包括: Partial(偏函数) — 该函数不包含对某些输入的返回处理(例如,可能因为无法处理的输入导致异常)。 Non-Deterministic(非确定性函数) — 该函数可能为相同的输入返回不同的输出。 Impure(非纯函数) — 该函数或者会产生副作用,或使用可变量或访问外部值。 与过程式程序不同,函数式程序只使用纯函数,它包含以下特征: Total(全函数) — 函数总是为每一种可能的输入返回一个输出。 Deterministic(确定性函数) — 函数总是为相同的输入返回相同的输出。 Pure(纯函数) — 函数的唯一效果是完全根据输入来决定输出。 纯函数仅以完全确定的方式将输入值组合或转换为输出值。纯函数更易于理解,更易于测试,更易于重构和更易于抽象。 函数式程序不会直接与外部世界交互,因为这样将会带来偏向性,不确定性和副作用。相反,函数式程序会构建并返回数据结构,并通过该数据结构描述(或建模)与现实世界的交互。 用于建模该过程 effect 的不可变数据结构称为函数式 effect。这个概念对于深入了解 ZIO 的工作原理至关重要,将在下一节中介绍。 程序就是值 我们可以仅用三个指令来构建一个描述控制台程序的数据结构: 在此模型中,Console[A]是一个不可变的,值类型安全的,返回 A 类型值的控制台程序。 下面这个 Console 数据结构是一个有序的“(语法)树”,在它“结尾”处,您会看到一条 Return 指令,该指令包含一个类型为 A 的值,该值是 Console[A] 程序的返回值。 尽管非常简单,但此数据结构足以构建交互式程序: 这个不变量值没有做任何事情——它只是描述一个程序,该程序打印出一条消息,请求输入,然后打印出另一条取决于输入的消息。 尽管此程序只是一个模型,但我们可以使用解释器将模型转换为过程效果,解释器将在数据结构上递归,将每条指令翻译为它描述的副作用: 解释(也称为运行或执行)不是函数式的,因为它可能是偏函数式的,非确定性的或不纯的。在理想的应用程序中,解释只需发生一次:在应用程序的主函数中。而该应用程序的其余部分应该完全是纯函数式的。 实际上,直接使用构造函数构建控制台程序不是很方便。相反,我们可以定义辅助函数,这些辅助函数看起来更像它们的 effect 等效物: 如果我们在 Console 上定义 map 和 flatMap 方法,则将这些“叶子”指令组合成较大的程序将变得更加容易: map 方法可通过提供函数 A => B 将返回 A 的 console 程序转换为返回 B 的控制台程序。 flatMap 方法使您可以按顺序将一个通过回调返回 A 类型结果的控制台程序和另一个将 A 作为输入的控制台程序组合在一起。 这两种函数的定义如下: 借助这些 map 和 flatMap 方法,我们现在可以利用 Scala 的  for comprehensions,编写看上去类似过程式的等效程序: 当我们希望执行该程序时,我们只需要在 Console 上调用解释器即可。 所有的函数式 Scala 程序都是这样构造的:与其直接与现实世界进行交互,不如建立一个函数式的 effect,这无非是一个对过程进行建模的不可变的,类型安全的,类似树的数据结构。 函数式程序员使用函数 effect 来构建复杂的现实世界软件,同时,不放弃纯函数式编程所提供的等式推理,可组合性和类型安全性。 Next Steps 如果函数式 effect 对您来说开始变得更有意义,那么下一步就是进一步了解 ZIO 中的核心效果类型。

运行 Effects

ZIO 提供了多种不同的方式在您的应用程序中运行您的 effect。 App 如果您将整个程序构建一个 effect,那么运行 effect 的最自然的方法是扩展zio.App。 此类提供了 Scala 的 main 函数,因此可以从IDE调用它,或从命令行启动它。您所需要做的就是实现 run 方法,命令行参数被保存在 List 中作为参数传递给它: 如果您自定义了应用程序的环境,则必须(使用 ZIO#provide来)为您的 effect 的 run 函数提供环境,因为您(自定义的)的 App不知道如何将自定义的环境提供给 effect。 缺省的运行时 大多数应用程序不是全新的,必须与遗留代码,过程库和框架集成。 在这些情况下,更好解决方案是创建一个运行时,将其传递给需要运行 effect 的地方。 ZIO 包含一个名为 Runtime.default 的默认运行时。此运行时将所有 ZIO 提供的模块的实现(包括 Console,System,Clock,Random,Scheduler以及在 JVM 上的 Blocking)捆绑在一起,并且可以运行需要这些模块任意组合的 effect。 要使用这个运行时,只需使用: 一旦您获得了一个运行时,你就可以将它用于执行您的 effect: 除了unsafeRun方法之外,还有其他方法可以异步执行 effect 或将其转换为Future。 定制 Runtime 如果您使用自定义环境运行程序,那么创建专门针对该环境量身定制的运行时可能会很有用。 以下两个值用来创建自定义的 custom Runtime[R]: R Environment. 这是执行 effect 时将提供的环境。This is the environment that will be provided to effects when they are executed. Platform. 这是 ZIO 启动运行时系统所需的平台。 例如,以下代码使用 ZIO 提供的默认平台创建了一个可以将 Int 作为环境值提供给effect 的运行时: 错误报告 作为运行时的一部分,每一个 platform 都包含一个错误报告程序,ZIO 将调用该错误报告程序以报告每个未经处理的错误。提供您自己的错误报告程序是个好主意,比如可以将未处理的错误记录到文件中。 默认的未处理错误报告器仅将错误输出到标准错误输出。 Next Steps 如果您对运行效果感到满意,那就恭喜! 现在,您可以学习 ZIO 网站的其他部分,包括数据类型,用例以及与其他系统的互操作。. 有关所有核心 ZIO 类型和方法的详细文档,请参考 Scaladoc。

Effects测试

有许多方法可以测试 effect,包括使用 free monad,使用 tagless-final 和使用环境 effect。尽管所有这些方法都与 ZIO 兼容,但最简单,最符合习惯的是 环境 effect。 本节介绍 环境 effect,并向您展示如何使用它们来编写可测试的功能代码。 环境 ZIO 数据类型有一个类型参数 R,它用来描述 effect 所需的环境类型。 ZIO effects 可以使用 ZIO.environment 来访问环境,通过它直接得到 R 类型的环境值: 环境不必是整数等原始类型。它可能要复杂得多,比如可以是一个 trait 或 case class。 如果环境带有属性字段,则可以通过 ZIO.access 单个调用直接访问环境的给定部分: 甚至 effect 本身也可以存储在环境中!在这种情况下,要访问和执行 effect,可以使用 ZIO.accessM 方法。 如上例所示,从环境访问 effect 时,该效果称为 environment effect。 稍后,我们将看到环境 effect 是怎样提供一种简便的方法来测试 ZIO 应用程序的。 提供 Environments 必须先为 effect 提供(providing)环境,然后它们才能运行。 最简单的为一个 effect 提供所需环境的方法是使用 ZIO#provide 函数: 您为 effect 提供了所需的环境后,如果其返回的 effect 的环境类型为 Any (UIO[_]),这表明其要求已完全得到满足。 ZIO.accessM 和 ZIO#provide 的组合对于充分地利用环境 effect 来使得测试变得简单是必需的。 环境化的 Effects 环境 effect 背后的基本思想是面向接口编程,而非面向实现。对于函数式语言 Scala 而言,接口不包含任何具有副作用的方法,但是它们可能包含返回函数化了的effect 的方法。 与其在整个代码库中手动地实现依赖注入或者使用不连贯的隐式来传递接口,不如使用 ZIO Environment 来进行繁重的工作,从而使代码优雅,可推断且轻松自如。 在本节中,我们将通过开发可测试的数据库服务来探索如何使用环境 effect。 定义服务(Service) 我们以模块的形式定义数据库服务,该模块是仅包含单个字段的接口,该字段提供对服务的访问。 在这个例子中,Database 代表 模块,它包含了 Database.Service 服务。 这个 服务 只是一个普通的接口,定义在模块的伴随对象中,包含了该服务提供的功能函数。 辅助函数 为了简化访问数据库服务的环境 effect,我们将定义一些辅助函数来调用 ZIO.accessM。 虽然这些辅助函数不是必须的,因为我们可以直接通过 ZIO.accessM 来访问数据库模块,但是这些帮助程序易于编写并且使得调用端的代码看上去更简单。 调取服务 我们已经定义了一个模块和辅助函数,现在我们准备构建一个使用数据库服务的示例: 在此示例中,effect 仅能通过环境与数据库进行交互,在这种情况下,环境就成为一个提供对数据库服务的访问的模块。 要实际运行这个 effect,我们仅需要提供数据库模块的实现。 实现服务实体 现在我们将实现数据库模块的服务实体,它将切实负责与我们的生产厂数据库的交互: 在以上这段代码片段中,我们不打算提供这两个数据库方法的实现,因为这将需要超出本教程的范围。 运行数据库 Effect 现在我们有了一个数据库模块,和于数据库模块交互的辅助方法,可以一个数据库模块的具体实现。 我们现在使用  ZIO.provide 将数据库模块的实现提交给我们的应用程序: 自此,我们对产生出的 effect 没有了进一步的要求,所以现在可以将它提交给运行时去执行了。 实现服务的测试案例 为了测试代码于数据库的交互,我们不需要提供一个真实的数据库,因为这样的话我们的测试将会很慢很脆弱,并且即便应用的逻辑都是正确的也可能产生随机性的错误。 虽然我们可以使用模拟(mocking)库来创建测试模块,但是在本节中,我们将直接直接创建一个测试模块,以表明这里面没有任何魔术: 由于此模块仅在测试中使用,因此我们通过硬编码读取和更新 map 中的数据来模拟于数据库的交互。为了测试模块具有纤程安全性,可以使用 Ref 而不是 var 来定义 map。 测试数据库代码 现在要测试请求数据库的代码,我们只需要为它提供这个用于测试的数据库模块就可以了: 我们的应用程序代码可以像测试数据库模块一样使用在生产数据库模块上。 Next Steps 如果您对测试效果感到满意,那么下一步就是学习运行 effects。

基本并发处理

ZIO 的底层使用了 纤程 来支持多并发。 纤程非常强大,但是它们运行在底层,为了提高效率,ZIO 提供了基于纤程的上层操作。 如果可能,你应该总是选择使用上层操作,而不是直接和纤程打交道。为了完整起见,本节将介绍两种纤程以及在其上构建的一些上层操作。 纤程 Fibers ZIO 的并发是构建在 纤程 上的,它是一种由 ZIO 运行时系统实现的轻量级的“绿色线程”。 和操作系统线程不同,纤程几乎不占用内存,具有可伸缩的堆栈,不会浪费资源,并且如果纤程处于挂起和无法访问状态,则会自动被垃圾回收。 纤程是由 ZIO 运行时调度的,并且会相互协作产生,即使在单线程环境(例如JavaScript,甚至是只配置一个线程的 JVM)中运行时,也可以实现多任务处理。 ZIO 中的所有效果都是由某个纤程执行的。如果你没有创建过纤程,那么纤程将由正在当前的操作(如果该操作是并发的或并行的)或由 ZIO 运行时系统自动创建。 即使您仅编写“单线程”代码,没有并行或并发操作,也将至少有一个纤程,作为执行 effect 的“主”纤程。 Fiber 数据类型 每条 ZIO 纤程都负责执行着某个 effect,并且由 ZIO 的 Fiber 数据类型来代表该运行计算的“句柄”。Fiber 数据类型于 Scala 的 Future 数据类型很类似。 Fiber[E, A] 数据类型在 ZIO 中有两个参数: E Failure Type. 如果纤程运行失败,将返回这种类型的值。 A Success Type. 如果纤程运行成功,则返回这种类型的值。 纤程并没有代表环境类型的 R 参数,以为它们执行的 effects 已经处于运行状态了,并且也已经为他们提供了所需的环境。 效果分支 创建纤程的最基本方法是对一个已存在的 effect 执行 fork 操作。 从概念上讲,效果分叉后会在新的纤程上开始执行,并且返回对新创建的 Fiber 具柄。 下面的代码创建了一个纤程来执行 fib(100): Joining Fibers 从 Fiber 中返回一个 effect 的方法之一,是使用 Fiber#join。由方法 Fiber#join 返回的纤程可以是成功的,也可以是失败的: Awaiting Fibers 另一种纤程返回的方法是 Fiber#await,它返回一个包含返回值的 effect,该值提供了有关纤程如何完成的完整信息。 纤程的中断 可以中断不再需要结果的纤程从而立即终止纤程的执行,安全地释放所有资源并运行所有的终结器。 和 await 一样, Fiber#interrupt 返回一个纤程如何完成退出的说明。 根据设计,由 Fiber#interrupt 返回的 effect 将不可恢复直到纤程彻底完成后。如果不需要恢复它,则可以对中断本身使用 fork: 纤程的组合 ZIO 允许您使用  Fiber#zip 或 Fiber#zipWith 来组合纤程。 这些方法将两个纤程合并为一个单个的纤程,并产生两者的结果。如果任一个纤程发生失败,则合成的纤程也将失败。 另一个组合纤程的方法是使用 Fiber#orElse 函数。如果第一个纤程成功,那么组合将返回该成功值,否则组合将执行并返回第二个纤程的返回值。(无论是否成功) 并行 ZIO 提供了很多操作用于实现 effect 的并行。这些方法都以 Par 作为后缀以有助于您判断何时将采用并行。 例如, 普通的 ZIO#zip 方法将两个 effect 按序列运算并合并在一起。但是还有一个 ZIO#zipPar 方法,可以将两个 effect 并行执行并合并在一起。 下表总结了一些顺序操作及其对应的并行版本: Description Sequential Parallel Zips two effects into one ZIO#zip ZIO#zipPar Zips two effects into one ZIO#zipWith ZIO#zipWithPar Collects from many effects ZIO.collectAll ZIO.collectAllPar Effectfully loop over values ZIO.foreach ZIO.foreachPar Reduces many values ZIO.reduceAll ZIO.reduceAllPar Merges many values ZIO.mergeAll ZIO.mergeAllPar 对于所有并行操作,如果一个 effect 失败,则其他 effect 也将被中断,以最大程度地减少不必要的计算。 如果不希望这种快速失败的行为,则可以先使用 ZIO#either 或 ZIO#option […]

BACK TO TOP