我读过两种关于STM如何实现的完全不同的说明。 也许两者都有效,或者一个是错误的,我希望有人能阐明。
取1(维基百科):允许所有线程修改共享内存,但事务中的每个读取和写入都会被记录。 在事务结束时,系统会检查其他线程是否未同时对内存进行更改。 如果未进行任何更改,则提交事务。 否则,事务将重新启动。
- 问:如果这是一个有效的实现,那么允许两个线程在同一事务中执行似乎毫无用处。 它们将读取彼此的写入,事务块内的计算将格式不正确。
取2(找不到源代码):当程序进入事务时,它会获得其中包含的所有变量的副本,并可以去镇上。 在事务结束时,系统会尝试使用副本更新主服务器。 如果自首次创建副本以来主节点未更改,则提交事务。 否则,事务将重新启动。
另外,当两个线程同时进入同一事务时会发生什么? 这不会导致某种竞争条件吗? 由于两者都试图修改相同的共享内存,因此两者都需要重新启动并且问题无限期地继续存在,除非系统介入并告诉一个线程冷却它(有点像锁=)我确定我在这里错过了一个概念。
我不是专家,但读过几篇论文。 与新技术提案一样,看起来一个小组提出了STM,但其他小组的后续工作着眼于变化。 不止你提到的两个。例如,本文给出了一种对事务具有阻塞锁定的机制,而不是你们两种方法中激进的非阻塞方法: http://pages.cs.wisc.edu/~david/courses/cs758/Fall2007/papers/saha-mcrt-ppopp-2007.pdf实现技术之间唯一的共同点似乎是类似数据库的事务语义。因此,研究的核心问题是这些语义是否会导致更好的程序和/或更高的效率。它们的实施方式有待商榷。
另外,当两个线程同时进入同一事务时会发生什么?这不会导致某种竞争条件吗?由于两者都在尝试修改相同的共享内存,因此两者都需要重新启动......
没有。系统始终取得进展,因为当 2 个或更多提交发生冲突时,日志允许所有提交回滚到冲突事务开始的位置。 然后,可以允许线程继续并按顺序提交。 你是对的,这会导致重复工作,尤其是当单个对象需求量很大时。 但是,只有当它比上下文交换成本更高时,避免这种情况才是值得的。 STM的人认为内存冲突相对罕见。维基百科方案在这个假设中特别激进。
这是一个答案的链接,它概述了TL2 STM算法,并提供了一些关于实现它以及如何机械地将普通代码转换为STM代码的相当可读的论文的链接:
Stackoverflow:你如何实现软件事务内存?
显然,这是一个活跃的研究领域,因此有很多方法。对于给定问题可能最有效的方法取决于手头问题的并发性和读/写比率。
关于TL2算法,您的问题:
。允许两个线程在同一线程中执行似乎毫无用处 交易。他们将读取彼此的写入和计算 在事务块内将格式不正确。
它关于在事务期间看到(读取集)或更新(写入集)的数据,而不是它们正在执行的代码。TL2 算法允许在提交之前不持有任何锁的情况下进行推理工作,使写入集对每个线程保持私有。在提交时,事务采用写锁定,然后执行最终版本检查读取集版本是否仍与主值版本(公共提交值)匹配,然后再刷新对主值的更新以递增其版本。当然,事务可以在提交之前看到自己的私有更新和任何提交的主值,但不同的 tx 看不到彼此未刷新的写入集。如果其他事务同时提交并更改了读取集中显示的主版本,则 tx 将中止。这将强制实施一致性,而无需在应用程序逻辑运行时保持锁。上面链接中的论文有更多细节和优化。
此外,当两个线程在 同时?这不会导致某种竞争条件吗?
是的,如果线程正在读取另一个线程将更新的值,则线程在执行不同的工作时会与 TL2 竞争。由于在最终一致性检查中中止,工作可能会被丢弃。您应该编写代码以在发生这种情况时重试多次,直到获得一致的读取,最终导致成功提交。希望是,如果您的服务器上有许多内核,则偶尔丢弃内核的工作将获得比使用阻止内核的路线粒度锁更多的总吞吐量。另一个希望是,STM可以比手工制作避免死锁的最佳细粒度锁定方法更直接。显然,吞吐量取决于实际的应用程序逻辑和冲突。
由于两者都试图修改相同的共享内存,因此两者都会 需要重新启动,问题无限期地继续,除非 系统介入并告诉一个线程冷却它(有点像锁=)
通过在提交时锁定主值来避免您提到的活锁。按照主值内存地址的升序排列所有锁,您就不会死锁。两个线程中的一个将首先获得所需的所有锁。另一个将阻止。然后,第一个线程将执行其读取集一致性检查,刷新其写集以递增主值,然后释放锁。第二个线程最终将获得它的所有锁,然后执行其读取集一致性检查,并中止事务,因为看到线程 1 增加了它在其应用程序逻辑中使用的主值的版本。
显然,此提交时间锁定策略会阻止线程,但在运行任意应用程序逻辑时不会保持锁定。这个关键部分可以聚合调整。这些论文甚至提到了处理器指令,这些指令要求线程在持有这些锁时保持不间断运行。