STM 介绍

STM 介绍

简介 ZIO 支持软件事务性内存 (STM),它是一种模块化的可组合的并发性数据结构。它允许我们在一个原子事务中组合并执行一组内存操作。 STM是一组并发任务之间的通信的抽象。STM 的主要优点是可组合性和模块化。我们可以编写可以与同样使用 STM 构建的任何其他抽象组合的并发抽象,同时不必暴露我们的抽象是如何确保安全的细节。而锁定机制则通常不是这样的。 Transactional 操作的想法并不新鲜,它们一直是分布式系统的基础,也是那些数据库之所以能够保证我们拥有 ACID 性质。STM是纯内存操作的。所有的操作都发生在内存中,与远程系统或数据库无关。与 ACID 属性的数据库概念非常相似,但缺少持久性,因为那对内存中的操作没有意义。 在事务性内存中,我们有关于 ACID 属性的几个方面: Atomicity(原子性)——在写操作中,我们需要原子更新,这意味着更新操作要么应该立即运行,要么根本不运行。 Consistency(一致性)——在读操作中,我们希望程序状态具有一致的视图,以确保各部分都引用相同的状态,在获得状态时获得相同的值。 Isolated(隔离性)——如果我们有多个同步更新,我们需要在隔离的事务中执行这些更新。每个事务不会影响其他并发事务。无论有多少纤程在运行多少数量的事务,都不必担心其它事务中发生的事情。 ZIO STM API 的灵感来自 Haskell 的 STM库,尽管 ZIO 中的实现完全不同。 问题 让我们从一个简单的inc函数开始,它接受一个 Int 类型的可变引用 并增加其 amount: 如果只有一个纤程,那不会有问题。这个函数看上去是正确的。但是,如果在读取计数器的值和设置新值之间,另一个纤程出现并改变了计数器的值,会发生什么?在我们读取计数器之后,另一个纤程恰好更新了计数器。所以这个函数是有竞争条件的,我们可以用下面的程序来测试: 由于上述程序运行了 10 个并发纤程来增加计数器值。但是,我们不能期望这个程序总是返回结果 10。 为了解决这个问题,我们需要原子地执行 get 和 set 操作。所幸 Ref 数据类型的一些 API,比如 update,updateAndGet和modify 提供了读写同步的原子操作。 需要注意的是有关 modify 操作最重要的一点是它不使用悲观锁定。它不对临界区使用任何锁定原语。它对潜在的操作碰撞有一个乐观的假设。 该 modify 函数执行以下三个步骤: 它假设其他纤程在大多数情况下不会改变共享状态并且在多数情况下不会互相干扰。因此它在不使用任何锁定原语的情况下读取共享状态。 你应该为最坏的情况做好准备。如果另一个纤程同时接入,会发生什么?因此,当我们开始写入新值时,应该检查所有可能影响的方面。它应该确保看到全局一致的状态,如果它看到了,那么它才可以改变那个值。 如果它遇到不一致的值,则不应继续。它应该使以上假设变得无效并中止更新共享状态。然后基于被修改过的值重新尝试 modify 操作。 让我们看看在没有任何锁定机制的情况下 Ref 如何实现 modify 功能: 正如我们所看到的,modify 操作是根据 compare-and-swap 操作来实现的,它帮助我们以原子方式执行读取和更新。 让我们将 inc 函数重命名为以下的 deposit,尝试将钱从一个账户转移到另一个账户的经典问题: 增加 withdraw 函数: 看起来还不错,但是实际上我们要先检查账户中是否有足够的余额可以提现。所以让我们修改它添加一个不变量来检查: 如果在检查和更新余额之间,另一条纤程来提取账户中的资金怎么办?所以这个解决方案包含一个错误。它有可能让账户达到负平衡。 假设我们最终达成了一个解决方案来自动退出,但问题仍然存在。我们需要以原子的方式将 withdraw 和 deposit 组合在一起以创建 transfer函数: 在上面的例子中,即使我们假设 withdraw 和 deposit 各自是原子的,我们也不能组合这两个事务。它们在并发环境中会产生错误。这段代码不能给我们保证 withdraw 都 deposit 两者在同一个原子操作中执行。同时执行此 transfer 方法的其他纤程可以覆盖共享状态并引入竞争条件。 我们需要一个解决方案来原子地组合事务。这就是STM发挥作用的地方。 可组合的并发 软件事务内存为我们提供了一种组合多个事务并在一个事务中执行它们的方法。 让我们继续我们的最后努力,将我们的 withdraw 方法转换为一个原子操作。为了使用 STM 来解决问题,我们将 Ref 替换 TRef. TRef 的含义是Transactional Reference;它是 STM 世界中的可变引用。STM 是一个一元的数据结构,代表一个可以事务化执行的 effect: 同样 deposit 操作是原子的,但为了能够和 withdraw 组合,我们也需要用 TRef 将其重构并 return STM: 在 STM 的世界中,我们可以组合所有操作,并直到世界的尽头,我们才在一个操作中原子地执行所有这些操作。为了能够让 withdraw 和 deposit 组合在一起,我们需要让它们都保持在 STM 的世界中。因此,我们不对它们中的每一个单独执行STM.atomically 或 STM#commit 方法。 现在我们可以在 STM 的世界中组合这两个函数来定义 transfer,并将它们转换为单一原子的 IO 方法: 假设我们正在将资金从一个账户转移到另一个账户。如果我们取出第一个账户但没有存入第二个账户,这种中间状态对任何外部纤程都是不可见的。如果不存在任何有冲突的变更,则事务完全成功。如果存在任何冲突或冲突更改,则整个 STM 将被重试。 它是如何工作的 STM 使用了和 Ref#modify 函数相同的思想,但具有可组合性特征。STM 的主要目标是提供一种机制来组合多个事务并在一个原子操作中执行它们。 可组合部分背后的机制是显而易见的。STM 有自己的世界。它有很多有用的 combinators(组合子),例如 flatMap 和orElse 可用于组合多个 STM 并创建更优雅的结果。在我们通过 STM#commit 或 […]

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 的语义,事务将因为没有钱而立即失败。

BACK TO TOP