我一直在对 STM(软件事务内存)实现进行一些研究,特别是关于利用锁且不依赖于垃圾收集器的存在的算法,以保持与 C/C++ 等非托管语言的兼容性。我已经阅读了 Herlihy 和 Shavit 的“多处理器编程艺术”中的 STM 章节,并阅读了 Shavit 的几篇描述他的“事务锁定”和“事务锁定 II”的论文STM 实现。他们的基本方法是利用存储全局版本时钟和锁的值的哈希表来确定内存位置是否已被另一个线程的写入触及。据我了解该算法,当执行写入事务时,版本时钟被读取并存储在线程本地内存中,并且还在线程本地内存中创建读取集和写入集。然后执行以下步骤:
- 读取的任何地址的值都存储在读取集中。这意味着事务检查正在读取的任何位置是否未锁定,并且它们等于或小于本地存储的版本时钟值。
- 写入的任何地址的值与要写入这些位置的值一起存储在写入集中。
- 一旦整个写入事务完成(这可能包括读取和写入多个位置),事务就会尝试使用哈希表中的锁锁定要写入的每个地址,该锁针对地址进行哈希处理' 价值。
- 当所有的写集地址都被锁定时,全局版本时钟原子地递增并且新的递增值被本地存储。
- 写入事务再次检查以确保读取集中的值没有用新的版本号更新或没有被另一个线程锁定。
- write-transaction 使用它在步骤 #4 中存储的新值更新该内存位置的 version-stamp,并将 write-set 中的值提交到内存
- 内存位置上的锁被释放
如果上述检查步骤中的任何一个失败(即步骤#1、#3 和#5),则写入事务被中止。
读取事务的过程要简单得多。根据 Shavit 的论文,我们简单地
- 读取并在本地存储全局版本时钟值
- 检查以确保内存位置的时钟值不大于当前存储的全局版本时钟值,并确保内存位置当前未锁定
- 执行读取操作
- 重复步骤 #2 进行验证
如果第 2 步或第 4 步失败,则中止读取事务。
不过,我似乎无法解决的问题是,当您尝试读取位于堆中的对象内的内存位置并且另一个线程调用delete
指向该对象的指针时会发生什么?在 Shavit 的论文中,他们详细解释了如何不能对已回收或释放的内存位置进行写入,但似乎在读取事务内部,没有什么可以阻止可能的时序场景允许您从已被另一个线程释放的对象内部的内存位置读取。例如,考虑以下代码:
Thread A
在原子读取事务中执行以下操作:linked_list_node* next_node = node->next;
Thread B
执行以下操作:delete node;
由于next_node
是线程局部变量,因此它不是事务对象。为它分配虽然的值所需的取消引用操作node->next
实际上需要两次单独的读取。在这些读取之间,delete
可以调用 on node
,以便从成员next
读取实际上是从已释放的内存段读取。node
由于读取是乐观的,因此in指向的内存的释放Thread B
不会被检测到 in Thread A
。这不会导致可能的崩溃或分段错误吗?如果是这样,如果不锁定内存位置以供读取,如何避免这种情况(教科书和论文都表示这是不必要的)?