有没有办法使用纯 python 为纯函数释放 GIL?

Fir*_*ger 5 python multiprocessing gil python-asyncio

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

假设你在 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)
Run Code Online (Sandbox Code Playgroud)

是否有一些内置功能或库提供某种类型的包装器,可以在函数执行期间释放 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)
Run Code Online (Sandbox Code Playgroud)

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

因为目前只对 I/O 密集型程序有吸引力的多线程,一旦这些功能长期运行就会变得有吸引力。做类似的事情

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)
Run Code Online (Sandbox Code Playgroud)

当然,multiprocessingofferPool.map可以做一些非常相似的事情。然而,如果函数返回一个非原始/复杂类型,那么工作线程必须序列化它,主进程必须反序列化并创建一个新对象,创建一个必要的副本。对于线程,子线程传递一个指针,主线程简单地获取对象的所有权。更快(更干净?)。

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

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

use*_*342 9

有没有办法使用纯 python 为纯函数释放 GIL?

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

GIL 不仅用于保护对象不被 Python 代码并发更新,其主要目的是防止解释器在访问和更新全局和共享数据时执行数据竞争(这是未定义的行为,即在 C 内存模型中被禁止) . 这包括Python的可见单身如NoneTrue,和False,而且所有全局像模块,共享类型的字典,和高速缓存。然后是它们的元数据,例如引用计数和类型对象,以及实现内部使用的共享数据。

考虑提供的纯函数:

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)
Run Code Online (Sandbox Code Playgroud)

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

>>> 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)
             ...
Run Code Online (Sandbox Code Playgroud)

要运行代码,解释器必须访问全局符号sincos调用它们。它访问整数 2、3、4、5、13 和 16,这些整数都已缓存,因此也是全局的。如果出现错误,它会查找异常类以实例化适当的异常。即使这些全局访问不修改对象,它们仍然涉及写入,因为它们必须更新引用计数

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