24

就实际的低级原子指令和内存栅栏(我假设它们已被使用)而言,您如何实现 STM?

对我来说神秘的部分是,给定一些任意代码块,您需要一种方法来返回并确定每个步骤中使用的值是否有效。你如何做到这一点,以及如何有效地做到这一点?这似乎也表明,就像任何其他“锁定”解决方案一样,您希望您的关键部分尽可能小(以减少冲突的可能性),对吗?

此外,STM 是否可以简单地检测“另一个线程在执行计算时进入该区域,因此计算无效”或者它是否可以实际检测是否使用了破坏值(因此幸运的是,有时两个线程可能同时执行相同的关键部分而没有需要回滚)?

4

5 回答 5

22

有些论文可能真的很难阅读,但有两篇非常清晰简洁:

Transactional Locking II,Dave Dice,Ori Shalev,Nir Shavit,它用任何语言描述了“TL2”STM 算法。

Deuce:Java 中的非侵入性软件事务内存,Guy Korland,Nir Shavit,Pascal Felber,解释了加载时间类转换器,它将常规 Java 类转换为内存中的类,这些类具有额外的字节码来执行 STM。这与该问题相关,因为该论文解释了如何将没有 STM 的代码机械地转换为在任何 OO 语言中执行 STM 的代码。

Deuce 框架允许您插入您希望使用的实际算法;包括TL2算法。由于 Deuce 框架是 Java,以下讨论使用 Java 术语,但仅假设您使用 OO 语言编写。

下面将概述 TL2 方法。这些论文有替代方法的链接,但研究一种算法可以回答很多问题。

你如何实现STM?对我来说神秘的部分是,给定一些任意代码块,您需要一种方法来返回并确定每个步骤中使用的值是否有效。

关于 TL2 如何处理 STM 的一个简短答案是“簿记”,然后仅在提交时使用写锁。阅读论文以获取详细信息,但板刷轮廓如下。您可以在原始代码中使用的每个属性都有一个 getter 和 setter。在转换后的代码中,还会有属性的版本号和添加到 getter 和 setter 的附加代码。您需要将您在事务中读取的每个属性的版本记录为事务“读取集”。您可以通过让每个 getter 将看到的属性版本添加到 threadlocal 链表中来做到这一点。您还需要将写入缓冲为线程本地中的“写入集”,直到您提交为止。请注意,getter 方法需要检查并返回给定字段的线程本地写入集值(如果有的话)。

在提交时,您对您将要写入的每个属性进行写入锁定。虽然您有锁,但您会仔细检查您的读取集是否仍然有效;您在事务中读取的属性没有被另一个事务更新到更高版本。如果是这样,那么您的业务逻辑可能无效,因为您可能会有不一致的读取,因此您需要回滚整个事务。如果最终检查通过,那么你通过刷新你的写集提交,碰撞这些属性的版本,释放写锁,最后清除写集和读集线程本地列表。

该论文解释说,如果算法检测到自 tx 开始以来已写入正在读取的属性,则该算法可以提前中止。该论文有一些巧妙的技巧来加速只读事务。它甚至有一个技巧来确定哪些块是只读的,哪些是读写的。任何对此类事物感兴趣的人都应该喜欢这两篇论文。

上面论文中的 Deuce 框架展示了如何通过在加载时将新的 java 字节码注入到类中来更改所有的 getter 和 setter。其他语言可能有一个特殊的编译器或预处理器,它们将普通代码执行相同的机械转换为支持 STM 的代码。特别是使用 Deuce,您的源代码对象可以具有简单的 getter setter 对,但在运行时转换的类具有丰富的 getter setter 来完成书本工作。

将常规代码转换为 STM 代码(尤其是在运行时)很有趣,但如果您需要实际编写启用 STM 的数据结构,则不需要任何魔法酱。相反,只需使用 a和 a创建一个Ref类,并使数据结构对象之间的每一个关系都由句柄组成。在你的课堂上,你可以做线程本地的书本。然后,您可以实现一个简单版本的“TL2”或任何其他适用于您的数据结构和读写并发的算法。 get()set(x)RefgetsetRef

这似乎也表明,就像任何其他“锁定”解决方案一样,您希望您的关键部分尽可能小(以减少冲突的可能性),对吗?

TL2 有一个关键时期是持有写锁,然后进行最终检查和写入,这很容易理解和优化,无需了解应用程序业务逻辑。如果您为每个数字分配一个唯一属性,您可以通过按升序获取锁来轻松避免死锁。重要的是要注意,假设提交检查将通过,您的所有业务逻辑都是推测性地完成的。在执行任意缓慢的业务逻辑时,您不会持有锁。您可以进行多个 Web 服务查找或缓慢的数据库调用,并且在提交之前您不会使用任何锁。显然,专业人士将调整通用关键部分。

该论文清楚地表明,该算法可能会比特定业务逻辑所需的更频繁地中止。通用算法不知道特定的脏读是否不会影响实际的写入结果。当给定的脏读集不需要回滚时,理解实际业务逻辑的手写逻辑可以知道特殊情况。但是,如果您有大量代码要编写,并且应用程序回滚的可能性非常低,那么通用的机械 STM 方法可能会导致更少的错误并表现良好。

此外,STM 是否可以简单地检测“另一个线程在执行计算时进入该区域,因此计算无效”或者它是否可以实际检测是否使用了破坏值(因此幸运的是,有时两个线程可能同时执行相同的关键部分而没有需要回滚)?

TL2 方法是关于读取或写入的数据,而不是关于执行它的代码。这是你得到和设置的,也是最重要的;以及在刷新所有写入之前是否有任何其他线程踩到您的脚趾。代码所需要的只是您在业务逻辑中有一个begin(),来启动、结束commit()rollback()中止事务。即使这样也可以生成代码。使用Java,您可以在方法上使用@Transactional 注释标记您的方法,然后生成将您的方法调用包装在try/catch/finally 中的代码,该代码将开始/提交/回滚作为惯用的java。Deuce 在类加载时注入这样的逻辑。

再一次,您不需要这样的魔法酱来在您自己的启用 STM 的数据结构中开始/提交/回滚。您可以明确地将所有内容放入您的数据结构逻辑代码中,以使用任何 OO 语言创建您自己的明确启用 STM 的类。

于 2012-09-08T20:58:08.587 回答
8

最简单的答案是“视情况而定”。有大量完全不同的实现以几乎任何可以想象的方式工作。

对我来说神秘的部分是,给定一些任意代码块,您需要一种方法来返回并确定每个步骤中使用的值是否有效。你如何做到这一点,以及如何有效地做到这一点?

一种解决方案是使用版本控制。每次修改对象时,都会更新其版本号。在事务运行时,您验证每个访问对象的版本,当事务提交时,您验证对象是否仍然有效。此验证可以是简单的整数比较(如果transaction_start_version >= object_version,则对象有效),因此可以相当有效地完成。

这似乎也表明,就像任何其他“锁定”解决方案一样,您希望您的关键部分尽可能小(以减少冲突的可能性),对吗?

极有可能。我认为一些实现已经走上了假设/要求一切都是事务的路线,但是是的,在大多数实现中,事务是特别标记的代码块,事务运行的时间越长,可能发生冲突的可能性就越大导致事务回滚。

此外,STM 是否可以简单地检测“另一个线程在执行计算时进入该区域,因此计算无效”或者它是否可以实际检测是否使用了破坏值(因此幸运的是,有时两个线程可能同时执行相同的关键部分而没有需要回滚)?

后者。请记住,TM 的理念是保护数据,而不是代码

不同的代码路径可以在不同的事务中访问同一个变量。这必须由 TM 系统检测到。没有“这个区域”的真正概念,因为它指的是代码,而不是数据。TM 系统不关心正在执行什么代码,它会跟踪正在修改的数据。这样,它完全不同于关键部分(保护代码,而不是数据)

于 2010-04-21T17:30:48.837 回答
7

GHC的 STM 实现在以下第六节中描述:

可组合内存事务。蒂姆·哈里斯、西蒙·马洛、西蒙·佩顿·琼斯、莫里斯·赫利希。PPoPP'05:ACM SIGPLAN 并行编程原理与实践研讨会,伊利诺伊州芝加哥,2005 年 6 月

第五节:

具有数据不变量的事务性内存。蒂姆·哈里斯,西蒙·佩顿-琼斯。2006 年 3 月交易 '06

于 2009-10-26T23:26:27.747 回答
4

我建议您观看此演示文稿:http: //www.infoq.com/presentations/Value-Identity-State-Rich-Hickey

在后半部分,它解释了如何更新值而不使它们处于未定义状态。例如 - 如果您有一个要以 STM 样式更新的树,则根本不需要更改以前的版本。假设这tree是一个指向树根的指针。您创建的唯一内容是更改的节点(但它们可以引用树的原始快照中的节点。

然后你对tree指针进行比较和交换。如果它成功了,那么现在每个人都会看到你的新树,而旧树可以被垃圾收集。如果没有,那么您重复该过程,您刚刚构建的树将被垃圾收集。

tree最大的想法是,在您实际交换新旧值之前,您不需要检测是否有其他人更改,因此典型的多线程编程中没有“冲突”或“破坏值”。

于 2009-11-08T20:05:51.147 回答
2

如果您要使用 .NET 框架,

你可以看看这个实验

于 2009-10-26T23:23:33.840 回答