软件事务内存

警告:以下内容相对过时,pypy-stm 变体不再积极开发。有关技术细节以及该方法的优缺点的描述,您可以阅读 Remi Meier 在 2019 年的 论文,以及他的 博士论文

此页面介绍的是 pypy-stm,这是一个正在开发中的特殊版本的 PyPy,它可以在同一个进程中并行运行多个独立的 CPU 密集型线程。它是针对 Python 世界中众所周知的“全局解释器锁 (GIL)”问题的解决方案——它是一个没有 GIL 的 Python 实现。

“STM”代表软件 事务内存,这是内部使用的技术。此页面从用户的角度描述了 pypy-stm,描述了正在进行的工作,最后给出了更多实现细节的参考。

这项工作由 Remi Meier 和 Armin Rigo 完成。感谢所有为这项工作提供资金的捐助者!请查看 第二次捐赠呼吁

pypy-stm 的用途

pypy-stm 是常规 PyPy 解释器的变体。(此版本支持 Python 2.7;有关 Python 3、CPython 和其他,请参见下文。)在下面列出的 注意事项 中,理论上它应该比常规 PyPy 慢 20%-50%,比较这两种情况下的 JIT 版本(但请参见下文!)。它被称为 STM,代表软件事务内存,这是内部使用的技术(参见 对实现细节的引用)。

好处是,生成的 pypy-stm 可以并行执行多个 Python 代码线程。理想情况下,并行运行两个或多个线程的程序应该比常规 PyPy 运行得更快(现在,或者随着错误修复而更快)。

  • pypy-stm 与基于 GIL 的 PyPy 完全兼容;您可以将其用作直接替换,多线程程序将在多个内核上运行。
  • pypy-stm 在纯 Python 模块 transaction 中为用户提供(但不强制)一个特殊的 API。该模块基于更底层的模块 pypystm,但也提供了一些与非 STM PyPy 或 CPython 的兼容性。
  • 基于 GIL 的移除方式,我们将讨论 如何编写多线程程序:10,000 英尺视角transaction.TransactionQueue

…以及 pypy-stm 不适合什么

pypy-stm 提供了一个没有 GIL 的 Python。这意味着它在 GIL 成为首要问题的场景中很有用。(这包括程序可以轻松修改为在多个线程中运行的场景;通常,我们不会考虑这样做,正是因为 GIL。)

然而,在许多情况下,GIL 并不是问题。不要指望 pypy-stm 在这些情况下有所帮助!这包括所有使用多个线程但实际上没有花费大量时间运行 Python 代码的程序。例如,它可能花费所有时间等待 I/O 发生,或对一个巨大的矩阵执行一些长时间的计算。在这些情况下,CPU 处于空闲状态,或者在某个 C/Fortran 库中;在这两种情况下,解释器(CPython 或常规 PyPy)都应该在外部调用周围释放 GIL。因此,线程不会争夺 GIL。

入门

pypy-stm 目前需要 64 位 Linux。

开发是在分支 stmgc-c8 中进行的。如果您只是想尝试一下,请一直催促我们,直到我们上传一个最新的预构建二进制文件。当前版本支持四个“段”,这意味着它将并行运行最多四个线程。

要从源代码构建版本,您首先需要编译一个自定义版本的 gcc(!)。请参阅此处的说明:https://bitbucket.org/pypy/stmgc/src/default/gcc-seg-gs/(请注意,这些补丁正在被合并到 gcc 中。未来版本的 gcc 很可能不再需要打补丁。)

然后获取 PyPy 的分支 stmgc-c8 并运行

cd pypy/goal
../../rpython/bin/rpython -Ojit --stm

最后,这将尝试通过调用 gcc-seg-gs 来编译生成的 C 代码,该代码必须是您在上述说明中安装的脚本。

当前状态 (stmgc-c7)

警告

此页面已过时,其余内容是关于 STMGC-C7,而当前的开发工作是在 STMGC-C8 上进行的

  • 新:它似乎运行良好,不再崩溃。请 报告您发现的任何崩溃(或其他错误)。
  • 它在“richards”等示例上的开销低至 20%。还有一些其他示例的开销更高——目前对于“translate.py”高达 2 倍——我们仍在努力理解。一个嫌疑人是我们的部分 GC 实现,见下文。
  • 新:PYPYSTM 环境变量和 pypy/stm/print_stm_log.py 脚本可以让您准确地知道发生了哪些“冲突”。这在下面 transaction.TransactionQueue 部分中进行了描述。
  • 新:特殊的交易友好 API(如 stmdict),在下面 transaction.TransactionQueue 部分中进行了描述。旧的 API 再次发生了变化,主要迁移到不同的模块。对此表示歉意。我认为尽早更改 API 比以后被一个糟糕的 API 困住要好……
  • 目前仅限于 1.5 GB 的 RAM(这只是一个参数,在 core.h 中——理论上。实际上,如果增加太多,clang 会再次崩溃)。内存溢出没有被正确处理;它们会导致段错误。
  • 新功能: JIT 预热时间再次提升,但仍然相对较长。为了生成机器代码,JIT 需要进入“不可避免”模式。这意味着,如果你的程序没有运行几秒钟,你将获得糟糕的性能结果,其中“几秒钟”可能意味着“很多秒钟”。在尝试基准测试时,请确保你已经达到了预热状态,即性能不再提升。
  • GC 是新的;虽然明显受到 PyPy 的常规 GC 的启发,但目前它缺少一些优化。分配大量不会立即死亡的小对象的程序(这肯定是一种常见情况)会受到这些缺失优化的影响。(最新的 stmgc-c8 在这方面表现更好。)
  • 弱引用现在可能看起来有点奇怪,有时会通过 gc.collect() 保持存活,甚至死亡,然后在再次死亡之前短暂地恢复。类似的问题偶尔会在其他地方出现,例如访问一些外部资源时,(明显的)序列化顺序与底层(多线程)顺序不匹配。这些是 bug(在 stmgc-c8 中已经部分修复)。此外,调试助手(如 weakref.getweakrefcount())可能会给出错误的答案。
  • STM 系统基于非常高效的读写屏障,这些屏障大多已经完成(它们的放置可以在 JIT 生成的机器代码中稍微改进)。
  • 分叉进程很慢,因为需要手动复制整个内存。会打印一条警告来提醒这一点。
  • 运行时间非常长的进程(以天为单位)最终会因内部 28 位计数器的未实现溢出而导致断言错误崩溃。
  • 递归检测代码没有重新实现。无限递归目前只会导致段错误。

Python 3、CPython 和其他

在本文件中,我描述了“pypy-stm”,它基于 PyPy 的 Python 2.7 解释器。支持 Python 3 大概需要一个下午的工作量。显然,我 *不* 意味着明天你就可以拥有一个完成且经过打磨的“pypy3-stm”产品。一般的 py3k 工作仍然缺失;一般的 stm 工作也仍然缺失。但它们通常彼此独立,就像 PyPy 中一样。现在内部接口似乎已经稳定,这个下午的工作量肯定会完成。

对于在 RPython 框架中实现的其他语言也是如此,尽管工作量可能会有所不同,因为 RPython 中的 STM 框架目前针对 PyPy 解释器,而其他解释器可能会有稍微不同的需求。但总的来说,所有繁琐的转换都由 RPython 完成,你只需要处理(希望很少的)困难且有趣的部分。

STM 的核心是一个用 C 编写的库(参见下面的 实现细节参考)。这意味着它可以在 RPython 生成的解释器之外使用。 Duhton 是一个早期的例子。此时,你可能会考虑将这个库移植到 CPython。不过,我要提醒你:据我所知,这是一个注定失败的想法。我在调试 Duhton 时遇到了很多困难,而 Duhton 比 CPython 简单得多。即使忽略这一点,你也可以在 Duhton 的 C 源代码中看到,许多核心设计决策与 CPython 不同:没有引用计数;对预构建的“静态”对象的有限支持;stm_read()stm_write() 宏调用无处不在(如果你忘记了一个,就会出现非常罕见且非常难以理解的 bug);等等。你可以想象一些 C 语言的自定义专用扩展,你可以将其预处理成常规的 C 代码。在我看来,这开始看起来很像 RPython 本身,但也许你更喜欢这种方法。当然,你仍然需要担心你需要的每个 C 扩展模块,但也许你会有一个前进的道路。

用户指南

如何编写多线程程序:10,000 英尺视角

PyPy-STM 提供两种编写多线程程序的方法

  • 传统方法,使用 threadthreading 模块,在 这里 描述。
  • 使用 TransactionQueue,在 这里 描述,作为一种隐藏线程底层概念的方法。

低级线程的问题众所周知(特别是在没有基于 GIL 的解释器的其他语言中):内存损坏、死锁、活锁等等。有一些替代方法可以处理线程,例如 OpenMP。这些方法通常会对你的代码施加一些结构。 TransactionQueue 在某种程度上类似:你的程序需要有“一些并行化的机会”才能应用它。但我相信 TransactionQueue 的适用范围比其他方法要大得多。它通常无需完全重组现有代码即可工作,并且适用于任何具有潜在不完美并行性的 Python 程序。理想情况下,它只需要最终程序员识别出这种并行性可能存在的位置,并使用简单的 API 将其传达给系统。

直接替换

多线程、CPU 密集型 Python 程序应该在 pypy-stm 上保持不变。它们将使用多个 CPU 内核并行运行。

GIL(全局解释器锁)的现有语义保持不变:尽管在多个内核上并行运行,但 pypy-stm 会让人感觉线程是串行运行的,切换只发生在字节码之间,而不是字节码中间。程序可以依赖这一点:使用 shared_list.append()/pop()shared_dict.setdefault() 作为同步机制将继续按预期工作。

这是通过在内部考虑标准 PyPy 或 CPython 释放 GIL 的位置,并将它们替换为“事务”的边界来实现的。就像数据库中的等效项一样,多个事务可以并行执行,但会以某种顺序提交。它们看起来像是按照这种序列化顺序完全运行的。

transaction.TransactionQueue

在 CPU 密集型程序中,我们通常可以轻松识别出一些数据结构上的最外层循环,或其他重复算法,其中每个“块”包含处理大量数据,并且这些块“很有可能”彼此独立。我们不需要证明它们实际上是独立的:只要它们经常独立就足够了——或者更准确地说,只要我们认为它们应该经常独立。

一个典型的例子如下所示,其中函数 func() 通常会调用大量代码

for key, value in bigdict.items():
    func(key, value)

然后,你只需将循环替换为

from transaction import TransactionQueue

tr = TransactionQueue()
for key, value in bigdict.items():
    tr.add(func, key, value)
tr.run()

这段代码的行为是等效的。在内部,TransactionQueue 对象将启动 N 个线程,并尝试在所有线程上并行运行 func(key, value) 调用。但请注意,这与许多比 Python 更底层的语言中发现的常规线程池库不同:函数调用不会仅仅因为它们并行运行而随机交织在一起。行为没有改变,因为我们使用了 TransactionQueue。所有调用仍然看起来以某种顺序执行。

使用 TransactionQueue 的典型情况是这样的:一开始,性能不会提高。事实上,它很可能更差。通常,这可以通过总 CPU 使用率来体现,它保持较低(更接近 1 而不是 N 个核心)。首先要注意,在 JIT 预热阶段,CPU 使用率不应明显高于 1 是正常的:您必须运行程序几秒钟,或者对于更大的程序至少运行一分钟,才能让 JIT 有机会充分预热。但是,如果即使之后 CPU 使用率仍然很低,那么可以使用 PYPYSTM 环境变量来跟踪正在发生的事情。

使用 PYPYSTM=logfile 运行您的程序以生成一个名为 logfile 的日志文件。之后,使用 pypy/stm/print_stm_log.py 实用程序来检查此日志文件的内容。它会生成类似于以下内容的输出(按丢失时间排序,最大的排在最前面)

10.5s lost in aborts, 1.25s paused (12412x STM_CONTENTION_WRITE_WRITE)
File "foo.py", line 10, in f
  someobj.stuff = 5
File "bar.py", line 20, in g
  someobj.other = 10

这意味着 10.5 秒的时间浪费在运行被中止的事务上(这又导致了 1.25 秒的暂停时间),因为两个独立的单条目回溯中显示的原因:一个线程运行了 someobj.stuff = 5 这行代码,而另一个线程同时在同一个对象上运行了 someobj.other = 10 这行代码。这两个写入操作都是针对同一个对象进行的。这会导致冲突,从而中止两个事务中的一个。在上面的示例中,这种情况发生了 12412 次。

另外两个冲突来源是 STM_CONTENTION_INEVITABLE,这意味着两个事务都尝试执行外部操作,例如打印或从套接字读取或访问外部原始数据数组;以及 STM_CONTENTION_WRITE_READ,这意味着一个事务写入了一个对象,而另一个事务只是读取了它,而不是写入它(在这种情况下,只报告写入事务;读取的位置没有记录,因为这样做会导致非常大的性能影响)。

冲突的常见原因

  • 首先,任何 I/O 或对内存的原始操作都会使事务不可避免(“不能中止”)。任何时候都只能运行一个不可避免的事务。一个常见的情况是,每个事务都从将数据发送到日志文件开始。您应该重构这种情况,使其发生在事务结束附近(然后可以主要以非不可避免模式运行),或者将其委托给单独的事务甚至单独的线程。

  • 写入列表或字典会与从同一个列表或字典中进行的任何读取发生冲突,即使是使用不同键进行的读取也是如此。对于字典和集合,您可以尝试使用 transaction.stmdicttransaction.stmset 类型,它们的行为与 dictset 类似,但允许对不同的键进行并发访问。(到目前为止,它们缺少的是延迟迭代:例如,stmdict.iterkeys() 被实现为 iter(stmdict.keys());并且,与 PyPy 的字典和集合不同,STM 版本不是有序的。)还有一些实验性的 stmiddictstmidset 类使用键的标识。

  • time.time()time.clock() 会使事务不可避免,以保证看起来较晚的调用确实会返回更高的数字。如果获得稍微无序的结果是可以的,请使用 transaction.time()transaction.clock()。后一种操作保证只在您可以“证明”两次调用按特定顺序发生时(例如,因为它们都由同一个线程调用)才会返回递增的结果。在没有这种证明的情况下,您可能会得到随机交织的值。(如果您有两个独立的事务,它们通常的行为就像其中一个事务完全执行完另一个事务一样;但使用 transaction.time(),您可能会看到它们实际上是交织的“隐藏真相”。)

  • transaction.threadlocalproperty 可在类级别使用

    class Foo(object):     # must be a new-style class!
        x = transaction.threadlocalproperty()
        y = transaction.threadlocalproperty(dict)
    

    这声明了 Foo 实例有两个属性 xy 是线程局部的:从并发运行的事务中读取或写入它们将返回独立的结果。(Foo 实例的任何其他属性将像往常一样从所有线程全局可见。)这与 TransactionQueue 一起使用对于以下两种情况很有用

    • 对于在一次事务期间发生变化但应始终重置为某个初始值的长期对象的属性(例如,在事务开始时初始化为 0;或者,如果用于此事务中待办事项列表,它将在每次事务结束时始终为空)。
    • 对于跨事务的一般缓存。使用 TransactionQueue,您将获得一个固定数量 N 个线程的池,每个线程按顺序运行事务。线程局部属性将具有由同一线程最后存储在其中的值,该值可能来自随机的先前事务。基本上,您将获得 N 个属性值的副本,并且每个事务都访问随机副本。它适用于缓存。

    更详细地说,threadlocalproperty() 的可选参数是默认值工厂:如果当前线程中尚未分配任何值,则会调用工厂,其结果将成为该线程中的值(如 collections.defaultdict)。如果未指定默认值工厂,则未初始化的读取将引发 AttributeError

  • 除了以上所有内容之外,还有一些情况会导致写写冲突,因为反复将相同的值写入属性。例如,参见 ea2e519614ab:这修复了两个此类问题,我们在其中写入对象字段而没有首先检查我们是否已经这样做。 dont_change_any_more 字段是一个标志,在代码的那部分设置为 True,但通常此 rtyper_makekey() 方法将被多次调用以获取相同的对象;代码过去会反复将标志设置为 True,但现在它首先检查,并且仅在为 False 时才执行写入。类似地,在检入的后半部分,方法 setup_block_entry() 用于同时分配 concretetype 字段并返回列表,但其两个调用者不同:一个确实需要 concretetype 字段初始化,而另一个只需要获取其结果列表——在这种情况下,concretetype 字段可能已经设置或未设置,但这无关紧要。

请注意,Python 是一种复杂的语言;有许多不常见的情况可能会导致冲突(任何类型的冲突),而我们可能在先验情况下没有预料到。在许多这些情况下,它可以被修复;请报告您不理解的任何情况。

原子部分

上面描述的 TransactionQueue 类基于原子部分,原子部分是您希望在不“释放 GIL”的情况下执行的代码块。在 STM 术语中,这意味着在保证事务不会在中间中断的情况下执行的代码块。这是实验性的,如果将来实现了 软件锁省略,可能会被删除

这是一个直接使用示例

with transaction.atomic:
    assert len(lst1) == 10
    x = lst1.pop(0)
    lst1.append(x)

在这个例子中,我们确定从列表一端弹出的元素会原子地附加到另一端。这意味着另一个线程可以运行 len(lst1)x in lst1 而不进行任何特定的同步,并且始终分别看到相同的结果,即 10True。它永远不会看到 lst1 仅包含 9 个元素的中间状态。原子部分类似于可重入锁(它们可以嵌套),但此外它们还可以防止任何代码的并发执行,而不仅仅是其他线程中碰巧受相同锁保护的代码。

请注意,原子部分的概念非常强大。如果你编写了这样的代码

with __pypy__.thread.atomic:
    time.sleep(10)

那么,如果你把它想象成我们有一个 GIL,你正在执行一个 10 秒长的原子事务,而根本没有释放 GIL。这会阻止所有其他线程继续执行。虽然这在 pypy-stm 中并不完全正确,但其他线程何时可以继续执行的确切规则相当复杂;你必须认为这样的代码最终可能会阻塞所有其他线程。

请注意,如果你想尝试使用 atomic,你可能需要在原子块之前手动添加一个事务中断。这是因为块的边界不一定是事务的边界:后者至少与块一样大,但可能更大。因此,如果你运行一个大型原子块,最好在之前中断事务。这可以通过调用 transaction.hint_commit_soon() 来完成。(这可能在某个时候得到修复。)

常规锁和原子块的交互也存在问题。如果你写入文件(它们有锁),包括使用 print 打印到标准输出,就会看到这一点。如果一个线程试图在原子块中获取锁,而另一个线程此时已经获得了相同的锁,那么前者可能会失败,并出现 thread.error。(不要依赖它;它也可能死锁。)原因是“等待”某个条件变为真——在原子块中运行——实际上没有意义。目前,你可以通过确保所有打印都在 atomic 块中或都不在其中来解决这个问题。(这种问题在理论上很难解决,可能是原子块支持最终被移除的原因。)

尚未实现

线程模块的锁其基本语义保持不变。但是,使用它们(例如在 with my_lock: 块中)会启动一种称为软件锁消除的替代运行模式。这意味着 PyPy 会尝试确保事务扩展到释放锁的点,如果成功,那么获取和释放锁将被“消除”。这意味着在这种情况下,整个事务在技术上不会导致对锁对象的任何写入——它之前未被获取,并且在事务之后仍然未被获取。

这在两个线程使用同一个锁运行 with my_lock: 块时特别有用。如果它们各自运行一个包含整个块的事务,那么对锁的所有写入都将被消除,并且两个事务不会相互冲突。像往常一样,它们将以某种顺序序列化:其中一个将出现在另一个之前运行。简单地说,它们各自在同一个事务中执行一个“获取”然后是一个“释放”。如上所述,锁状态从“未获取”变为“未获取”,因此可以保持不变。

这种方法可以优雅地失败:与原子部分不同,没有保证事务会一直运行到块的末尾。如果你在持有锁时执行任何输入/输出,事务将在输入/输出操作之前像往常一样结束。如果发生这种情况,那么锁消除模式将被取消,并且锁的“已获取”状态将被真正写入。

即使锁已经被真正获取,事务也不必等待它再次释放。它可以进入省略模式,并尝试执行块的内容。只有在最后,当尝试提交时,线程才会暂停。一旦存储在锁中的实际值切换回“未获取”,它就可以继续并尝试提交它已经执行的事务(该事务可能失败并中止,并从头开始重新启动,如往常一样)。

请注意,这尚未实现,但我们预计即使您获取和释放多个锁,它也能正常工作。省略模式事务将一直持续到您获取的第一个锁被释放,或者直到代码执行输入/输出或等待操作(例如,等待当前不可用的另一个锁)。在嵌套顺序获取多个锁的常见情况下,它们将全部被同一事务省略。

杂项函数

  • 首先,请注意 transaction 模块位于文件 lib_pypy/transaction.py 中。此文件可以复制到其他地方,以便在 CPython 或非 STM PyPy 上执行相同的程序,并提供回退行为。(行为不同的一个情况是 atomic,在回退情况下它只是一个普通的锁;因此 with atomic 只能阻止其他线程进入其他 with atomic 部分,但不会阻止其他线程运行非原子代码。)
  • transaction.getsegmentlimit():返回此 pypy-stm 中的“段”数量。这是更多线程无法在更多核心上执行的限制。(目前它限制为 4,因为段间开销,但将来应该增加。它也应该可以设置,默认值应该取决于实际 CPU 的数量。)如果 STM 不可用,则返回 1。
  • __pypy__.thread.signals_enabled:一个上下文管理器,它在启用信号的情况下运行其代码块。默认情况下,信号仅在主线程中启用;非主线程不会接收信号(这与 CPython 相似)。在非主线程中启用信号对于库很有用,在这些库中线程是隐藏的,最终用户不希望他的代码在主线程之外的其他地方运行。
  • pypystm.exclusive_atomic:一个类似于 transaction.atomic 的上下文管理器,但如果嵌套则会报错。
  • transaction.is_atomic():如果从原子上下文中调用,则返回 True。
  • pypystm.count():每次调用时返回一个不同的正整数。这在不产生冲突的情况下工作。返回的整数只是大致按升序排列;不应依赖于此。

有关冲突的更多详细信息

基于软件事务内存,pypy-stm 解决方案容易出现“冲突”。为了重复基本思想,线程推测性地执行它们的代码,并在已知点(例如字节码之间)相互协调,以就各自操作的顺序达成一致,即“提交”,即在全局范围内可见。两次提交点之间的每个持续时间称为事务。

当没有一致的排序时,就会发生冲突。经典的例子是,如果两个线程都尝试更改同一个全局变量的值。在这种情况下,只允许其中一个线程继续,另一个线程必须暂停或中止(重新启动事务)。如果这种情况过于频繁,并行化就会失败。

多线程程序可以实现多少实际的并行化有点微妙。基本上,一个不使用 transaction.atomic 或省略锁,或者只在很短的时间内这样做,程序将几乎可以自由地并行化(只要它不是一些人为的例子,例如,所有线程都尝试增加同一个全局计数器,而没有做其他事情)。

但是,如果程序需要更长时间的事务,它就会带来不太明显的规则。确切的细节也可能因版本而异,直到它们稳定下来。以下是一个概述。

并行化只要遵循两个原则就能正常工作。第一个原则是事务之间不能发生冲突。最明显的冲突来源是所有线程都递增一个全局共享计数器,或者所有线程都将计算结果存储到同一个列表中——或者,更微妙的是,所有线程都从同一个列表中pop()要执行的工作,因为这也会修改列表。(你可以使用transaction.stmdict来解决这个问题,但对于这个特定的例子,最终应该设计一些支持 STM 的队列。)

冲突的发生方式如下:当一个事务提交(即成功完成)时,它可能会导致其他正在进行的事务中止并重试。这会浪费 CPU 时间,但即使在最坏的情况下,它也不会比 GIL 更糟糕,因为至少有一个事务会成功(因此,在最坏的情况下,我们有 N-1 个 CPU 在做无用功,而 1 个 CPU 在做成功提交的工作)。

当然,冲突确实会发生,试图避免所有冲突是毫无意义的。例如,在某些预热阶段,冲突可能非常普遍。重要的是要确保冲突在总体上保持在较低的频率。

另一个问题是避免长时间运行的所谓“不可避免”的事务(“不可避免”是指“无法避免”,即无法再中止的事务)。只有在使用atomic时才会发生这类事务,通常是因为原子块中的 I/O 操作。它们可以正常工作,但在执行 I/O 操作之前,事务会变成不可避免的。在原子块的剩余执行时间内,它们会阻碍并行工作。最好的做法是将代码组织起来,以便这类操作完全在atomic之外执行。

(这与 Twisted 不鼓励阻塞 I/O 操作的事实并不矛盾,如果你确实需要它们,你应该在单独的线程上执行它们。)

如果锁消除最终取代了原子部分,我们不会遇到长时间运行的不可避免事务,但同样的问题会以不同的方式出现:执行 I/O 会取消锁消除,锁会变成真正的锁。这会阻止其他线程提交,如果它们也需要这个锁。(当锁消除被实现和测试后,会对此进行更多说明。)

实现

XXX 此部分目前大部分为空

技术报告

STMGC-C7 在一篇技术报告中进行了详细描述。

一篇单独的立场文件概述了我们对 STM 的总体立场。

实现细节参考

实现的核心是一个名为stmgc的独立 C 库,位于c7子目录(pypy-stm 的当前版本)和c8子目录(最新版本)。请参阅README.txt以获取更多信息。特别是,其中讨论了段的概念。

PyPy 本身在它之上添加了屏障以及“现在变得不可避免”屏障的自动放置,作为 RPython 转换的启动/停止事务的逻辑,以及作为支持C 代码的逻辑,以及 JIT 中的支持(主要作为跟踪上的转换步骤以及在assembler.py中生成自定义汇编器)。