有没有一种方法可以使用纯python为纯函数发布GIL



我想我一定错过了什么;这似乎很正确,但我想不出办法做到这一点。

假设您在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绑定程序有吸引力,一旦这些函数长时间运行,就会对它们有吸引力。做一些类似的事情

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,它可以做一些非常类似的事情。但是,如果函数返回非基元/复杂类型,则辅助进程必须序列化它,而主进程必须反序列化并创建新对象,从而创建必要的副本。对于线程,子线程传递一个指针,而主线程只是获取对象的所有权。更快(更干净?(。

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

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

有没有一种方法可以使用纯python为纯函数发布GIL?

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

GIL不仅用于保护对象不被Python代码同时更新,它的主要目的是防止CPython解释器在访问和更新全局和共享数据时执行数据竞赛(这是一种未定义的行为,即在CPython执行的C内存模型中被禁止(。这包括Python可见的singleton,如NoneTrueFalse,但也包括所有全局,如模块、共享dict和缓存。然后是它们的元数据,如引用计数和类型对象,以及实现内部使用的共享数据。

考虑提供的纯功能:

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工具揭示了解释器在执行函数时执行的操作:

>>> 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,这些整数都是缓存的,因此也是全局的。在出现错误的情况下,它会查找异常类,以便实例化适当的异常。即使这些全局访问不修改对象,它们仍然涉及写入,因为它们必须更新引用计数。

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

相关内容

  • 没有找到相关文章

最新更新