乐观读取并使用 C/C++锁定 STM(软件事务内存)

Optimistic reads and Locking STM (Software Transactional Memory) with C/C++

本文关键字:STM 软件 事务 内存 锁定 C++ 读取 乐观      更新时间:2023-10-16

我一直在对STM(软件事务内存(实现进行一些研究,特别是关于利用锁并且不依赖于垃圾收集器存在的算法,以保持与C/C++等非托管语言的兼容性。 我读过Herlihy和Shavit的"多处理器编程的艺术"中的STM章节,并阅读了Shavit的几篇论文,描述了他的"事务锁定"和"事务锁定II"STM实现。 他们的基本方法是利用存储全局版本时钟和锁值的哈希表来确定内存位置是否已被另一个线程的写入所触及。 据我了解该算法,当执行写入事务时,版本时钟被读取并存储在线程本地内存中,并且读取集和写集也在线程本地内存中创建。然后执行以下步骤:

    读取
  1. 的任何地址的值都存储在读取集中。 这意味着事务会检查正在读取的任何位置是否未锁定,并且它们等于或小于本地存储的版本时钟值。
  2. 写入
  3. 的任何地址的值以及要写入这些位置的值都存储在写入集中。
  4. 一旦整个写入事务完成(这可能包括读取和写入多个位置(,事务将尝试使用哈希表中针对地址值进行哈希处理的锁来锁定要写入的每个地址。
  5. 当所有写集地址都被锁定时,全局版本时钟将以原子方式递增,新的递增值将在本地存储。
  6. 写入事务再次检查以确保读取集中的值未使用新版本号更新或未被另一个线程锁定。
  7. 写入事务使用它在步骤 #4 中存储的新值更新该内存位置的版本戳,并将写入集中的值提交到内存
  8. 释放内存位置上的锁

如果上述任何检查步骤失败(即步骤 #1、#3 和 #5(,则写入事务将中止。

读取事务的过程要简单得多。 根据Shavit的论文,我们只是简单地

  1. 读取并本地存储全局版本时钟值
  2. 检查以确保内存位置的时钟值
  3. 不大于当前存储的全局版本时钟值,并确保内存位置当前未锁定
  4. 执行读取操作
  5. 重复步骤 #2 进行验证

如果步骤 #2 或 #4 失败,则读取事务中止。

不过,我似乎无法在脑海中解决的问题是,当您尝试读取位于堆中的对象内的内存位置时会发生什么,而另一个线程在指向该对象的指针上调用delete? 在Shavit的论文中,他们详细解释了为什么不能写入已回收或释放的内存位置,但似乎在读事务内部,没有什么可以阻止可能的计时方案,该场景允许您从已由另一个线程释放的对象内部的内存位置读取。 例如,请考虑以下代码:

Thread A在原子读取事务中执行以下内容:linked_list_node* next_node = node->next;

Thread B执行以下操作:delete node;

由于next_node是线程局部变量,因此它不是事务对象。 为其分配值所需的取消引用操作 node->next 实际上需要两次单独的读取。 在这些读取之间,可以在node上调用delete,以便从成员next读取实际上是从已经释放的内存段读取。 由于读取是乐观的,因此Thread Bnode指向的内存释放不会在Thread A中检测到。 这不会导致可能的崩溃或分段错误吗? 如果是这样,如何在不锁定阅读的内存位置的情况下避免这种情况(教科书和论文都表示这是不必要的(?

简单的答案是delete是一种副作用,交易不会很好地产生副作用。

从逻辑上讲,由于事务可以随时回滚,因此不能在事务中间释放内存。

我认为"如何处理"没有一个通用的答案,但一种常见的方法是将delete调用推迟到提交时间。STM API 应该自动执行此操作(例如提供自己的 delete 函数并要求您执行此操作(,或者为您提供一个钩子,您可以在其中注册"提交时要执行的操作"。然后,在事务期间,您可以注册要在事务提交时删除的对象。

然后,处理已删除对象的任何其他事务都应无法通过版本检查并回滚。

希望有帮助。一般来说,副作用没有一个简单的答案。这是每个单独的实现都必须提出来处理的机制。