有没有办法使用纯 python 为纯函数发布 GIL?

Is there a way to release the GIL for pure functions using pure python?

我想我一定是漏掉了什么;这似乎是正确的,但我看不出有什么办法可以做到这一点。

假设你在 Python:

中有一个纯函数
from math import sin, cos

def f(t):
    x = 16 * sin(t) ** 3
    y = 13 * cos(t) - 5 * cos(2*t) - 2 * cos(3*t) - cos(4*t)
    return (x, y)

是否有一些内置功能或库提供了某种包装器,可以在函数执行期间释放 GIL?

在我看来,我在想一些类似

的事情
from math import sin, cos
from somelib import pure

@pure
def f(t):
    x = 16 * sin(t) ** 3
    y = 13 * cos(t) - 5 * cos(2*t) - 2 * cos(3*t) - cos(4*t)
    return (x, y)

为什么我认为这可能有用?

因为目前只对 I/O-bound 程序有吸引力的多线程,一旦它们变得 long-运行ning 就会对这些函数有吸引力。做类似

的事情
from math import sin, cos
from somelib import pure
from asyncio import run, gather, create_task

@pure  # releases GIL for f
async def f(t):
    x = 16 * sin(t) ** 3
    y = 13 * cos(t) - 5 * cos(2 * t) - 2 * cos(3 * t) - cos(4 * t)
    return (x, y)


async def main():
    step_size = 0.1
    result = await gather(*[create_task(f(t / step_size))
                            for t in range(0, round(10 / step_size))])
    return result

if __name__ == "__main__":
    results = run(main())
    print(results)

当然,multiprocessing 提供 Pool.map 可以做非常相似的事情。但是,如果函数 returns 是非原始/复杂类型,则工作人员必须对其进行序列化,并且主进程必须反序列化并创建一个新对象,从而创建一个必要的副本。对于线程,子线程传递一个指针,主线程简单地取得对象的所有权。更快(更干净?)。

将此与我几周前遇到的一个实际问题联系起来:我正在做一个强化学习项目,其中涉及为类似国际象棋的游戏构建 AI。为此,我正在模拟 AI 与自己进行 > 100,000 游戏;每次返回棋盘状态的结果序列(numpy 数组)。循环生成这些游戏 运行,我每次都使用这些数据创建更强大的 AI 版本。在这里,重新创建 ("malloc") 主进程中每个游戏的状态序列是瓶颈。我尝试重新使用现有对象,出于多种原因这是一个坏主意,但并没有产生太大的改进。

编辑:这个问题不同于 How to run functions in parallel? ,因为我不只是在寻找并行 运行 代码的任何方法(我知道这可以通过多种方式实现,例如通过 multiprocessing).我正在寻找一种方法让解释器知道在并行线程中执行此函数时不会发生任何坏事。

Is there a way to release the GIL for pure functions using pure python?

简而言之,答案是,因为这些函数在 GIL 运行的级别上并不纯粹。

GIL 不仅用于保护对象不被 Python 代码同时更新,它的主要目的是防止解释器执行 data race (which is undefined behavior,即在 C 内存模型中被禁止)同时访问和更新全局和共享数据。这包括 Python-可见的单例,例如 NoneTrueFalse,但也包括所有全局变量,例如模块、共享字典和缓存。然后是它们的元数据,例如引用计数和类型对象,以及实现内部使用的共享数据。

考虑提供的纯函数:

def f(t):
    x = 16 * sin(t) ** 3
    y = 13 * cos(t) - 5 * cos(2*t) - 2 * cos(3*t) - cos(4*t)
    return (x, y)

dis tool揭示了解释器在执行函数时进行的操作:

>>> dis.dis(f)
  2           0 LOAD_CONST               1 (16)
              2 LOAD_GLOBAL              0 (sin)
              4 LOAD_FAST                0 (t)
              6 CALL_FUNCTION            1
              8 LOAD_CONST               2 (3)
             10 BINARY_POWER
             12 BINARY_MULTIPLY
             14 STORE_FAST               1 (x)
             ...

到运行代码,解释器必须访问全局符号sincos才能调用它们。它访问整数 2、3、4、5、13 和 16,它们都是 cached and therefore also global. In case of an error, it looks up the exception classes in order to instantiate the appropriate exceptions. Even when these global accesses don't modify the objects, they still involve writes because they must update the reference counts.

None 其中的

None 可以在没有同步的情况下从多个线程安全地完成。虽然可以想象修改 Python 解释器以实现不访问全局状态的真正纯函数,但这需要对内部进行重大修改,影响与现有 C 扩展的兼容性,包括广受欢迎的科学扩展。最后一点是事实证明移除 GIL 如此困难的主要原因。