Python3.9 多处理导致 MacOS 上的范围错误
Python3.9 multiprocessing results in scoping error on MacOS
我在 iMac(2011,macOS High Sierra,英特尔酷睿 i7 2600,Python3.9.2)上遇到了一个原始脚本 运行 错误,因此使用下面的简单代码重现了行为:
from multiprocessing import Pool
import time
def raise_to_power(number):
number = number ** power
return number
if __name__ == '__main__':
start_time = time.time()
threads = 8
power = 10
numbers = [x for x in range(1,1000000)]
chunksize, rem = divmod(len(numbers), threads)
if rem: chunksize += 1
with Pool(threads) as p:
Product = p.map(raise_to_power,numbers,chunksize)
print(time.time() - start_time)
iMac 上的输出给我:
multiprocessing.pool.RemoteTraceback:
"""
Traceback (most recent call last):
File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/multiprocessing/pool.py", line 125, in worker
result = (True, func(*args, **kwds))
File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/multiprocessing/pool.py", line 48, in mapstar
return list(map(*args))
File "/Users/yn/PycharmProjects/test-multiprocessing/test-multiprocessing_thinkpad2.py", line 6, in raise_to_power
number = number ** power
NameError: name 'power' is not defined
"""
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/Users/yn/PycharmProjects/test-multiprocessing/test-multiprocessing_thinkpad2.py", line 18, in <module>
Product = p.map(raise_to_power,numbers,chunksize)
File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/multiprocessing/pool.py", line 364, in map
return self._map_async(func, iterable, mapstar, chunksize).get()
File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/multiprocessing/pool.py", line 771, in get
raise self._value
NameError: name 'power' is not defined
相同的代码在 Thinkpad t420(Linux Mint 20,core i5 2540m,Python3.9.2)上正确运行,执行时间约为 0.6-0.7 秒。
我重写了代码以克服范围界定错误:
from multiprocessing import Pool
import time
from itertools import product
def raise_to_power(number, power):
number = number ** power
return number
if __name__ == '__main__':
start_time = time.time()
threads = 8
power = 10
numbers = [x for x in range(1,1000000)]
chunksize, rem = divmod(len(numbers), threads)
if rem: chunksize += 1
with Pool(threads) as p:
Product = p.starmap(raise_to_power,product(numbers,[power]),chunksize)
print(time.time() - start_time)
现在它可以在两个系统上正确运行(iMac 平均只快 0.15 秒),但我没有在多处理文档中找到任何关于所描述行为的线索。能指出来吗?
让我们看看您的原始方法,其中 power
未定义。首先,有几点评论。您正在调用辅助函数 multiply_by
,但我也没有看到它的定义位置。但是,我确实看到了函数 raise_to_power
。您应该得到的错误是 multiply_by
未定义。所以这有点令人费解。其次,我看到您正在计算 chunksize
而不是使用默认函数来执行此操作,后者计算的值大约是您计算的大小的 1/4。较大的块大小意味着较少的内存传输(好),但如果处理器不以相同的速率(坏)处理任务,则可能导致处理器最终空闲,这在您的情况下不太可能。我可以看到你有自己的函数来计算块大小,因为 map
方法使用的函数必须在必要时将其可迭代参数转换为列表以获得它的长度,如果可迭代非常大,这可能是内存效率非常低。但是您已经将 range
转换为列表,因此您没有利用自己计算来节省内存的机会。
正如 Mark Satchell 指出的那样,针对您的情况的简单解决方案就是使 power
成为全球性的。但是让我们考虑一般情况。如果您的平台是 Windows 或任何使用 spawn
来创建新进程的平台(我猜这很可能是基于您使用 if __name__ == '__main__':
管理创建新进程的代码的情况),那么在全局范围内的任何代码都将针对每个创建的新进程执行。对于像 power = 10
这样的语句来说这不是问题。但是如果 power
需要更复杂的代码来初始化它的值,那么为进程池中的每个进程一遍又一遍地重新执行这段代码将是低效的。或者考虑 power
是一个非常大的数组的情况。在每个子进程的内存中创建这个数组的实例可能成本太高 space。那么需要的是共享内存中数组的单个实例。
有一种机制可以在创建时使用 initializer 和 initargs 参数为池中的每个子进程初始化全局变量Pool
实例。我还利用您 是 使用自己的块大小计算的事实进行了额外的更改以节省内存:
from multiprocessing import Pool
import time
def init_pool(the_power):
""" Initialize each process's global variable power. """
global power
power = the_power
def raise_to_power(number):
number = number ** power
return number
if __name__ == '__main__':
start_time = time.time()
threads = 8
power = 10
FROM = 1
TO = 1000000
cnt = TO - FROM
# changed to a generator expression to save memory:
#numbers = (x for x in range(FROM, TO))
# or just:
numbers = range(FROM, TO)
chunksize, rem = divmod(cnt, threads)
if rem: chunksize += 1
# initialize each pool process with power:
with Pool(threads, initializer=init_pool, initargs=(power,)) as p:
Product = p.map(raise_to_power,numbers,chunksize)
print(time.time() - start_time)
进一步说明
在使用 OS 调用 spawn 而非 fork 来创建新进程的平台上创建子进程时,新进程不会继承已在父进程。相反,执行从程序的顶部开始。但是因为你 if __name__ == '__main__':
保护了创建新进程和初始化 power 的代码(这是避免递归循环无限地重新创建新进程所必需的),语句 power = 10
永远不会在子流程和 voilà。当您将 power = 10
移到 if __name__ == '__main__':
之外时,即使创建了子流程,它也会被执行。
我在 iMac(2011,macOS High Sierra,英特尔酷睿 i7 2600,Python3.9.2)上遇到了一个原始脚本 运行 错误,因此使用下面的简单代码重现了行为:
from multiprocessing import Pool
import time
def raise_to_power(number):
number = number ** power
return number
if __name__ == '__main__':
start_time = time.time()
threads = 8
power = 10
numbers = [x for x in range(1,1000000)]
chunksize, rem = divmod(len(numbers), threads)
if rem: chunksize += 1
with Pool(threads) as p:
Product = p.map(raise_to_power,numbers,chunksize)
print(time.time() - start_time)
iMac 上的输出给我:
multiprocessing.pool.RemoteTraceback:
"""
Traceback (most recent call last):
File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/multiprocessing/pool.py", line 125, in worker
result = (True, func(*args, **kwds))
File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/multiprocessing/pool.py", line 48, in mapstar
return list(map(*args))
File "/Users/yn/PycharmProjects/test-multiprocessing/test-multiprocessing_thinkpad2.py", line 6, in raise_to_power
number = number ** power
NameError: name 'power' is not defined
"""
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/Users/yn/PycharmProjects/test-multiprocessing/test-multiprocessing_thinkpad2.py", line 18, in <module>
Product = p.map(raise_to_power,numbers,chunksize)
File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/multiprocessing/pool.py", line 364, in map
return self._map_async(func, iterable, mapstar, chunksize).get()
File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/multiprocessing/pool.py", line 771, in get
raise self._value
NameError: name 'power' is not defined
相同的代码在 Thinkpad t420(Linux Mint 20,core i5 2540m,Python3.9.2)上正确运行,执行时间约为 0.6-0.7 秒。
我重写了代码以克服范围界定错误:
from multiprocessing import Pool
import time
from itertools import product
def raise_to_power(number, power):
number = number ** power
return number
if __name__ == '__main__':
start_time = time.time()
threads = 8
power = 10
numbers = [x for x in range(1,1000000)]
chunksize, rem = divmod(len(numbers), threads)
if rem: chunksize += 1
with Pool(threads) as p:
Product = p.starmap(raise_to_power,product(numbers,[power]),chunksize)
print(time.time() - start_time)
现在它可以在两个系统上正确运行(iMac 平均只快 0.15 秒),但我没有在多处理文档中找到任何关于所描述行为的线索。能指出来吗?
让我们看看您的原始方法,其中 power
未定义。首先,有几点评论。您正在调用辅助函数 multiply_by
,但我也没有看到它的定义位置。但是,我确实看到了函数 raise_to_power
。您应该得到的错误是 multiply_by
未定义。所以这有点令人费解。其次,我看到您正在计算 chunksize
而不是使用默认函数来执行此操作,后者计算的值大约是您计算的大小的 1/4。较大的块大小意味着较少的内存传输(好),但如果处理器不以相同的速率(坏)处理任务,则可能导致处理器最终空闲,这在您的情况下不太可能。我可以看到你有自己的函数来计算块大小,因为 map
方法使用的函数必须在必要时将其可迭代参数转换为列表以获得它的长度,如果可迭代非常大,这可能是内存效率非常低。但是您已经将 range
转换为列表,因此您没有利用自己计算来节省内存的机会。
正如 Mark Satchell 指出的那样,针对您的情况的简单解决方案就是使 power
成为全球性的。但是让我们考虑一般情况。如果您的平台是 Windows 或任何使用 spawn
来创建新进程的平台(我猜这很可能是基于您使用 if __name__ == '__main__':
管理创建新进程的代码的情况),那么在全局范围内的任何代码都将针对每个创建的新进程执行。对于像 power = 10
这样的语句来说这不是问题。但是如果 power
需要更复杂的代码来初始化它的值,那么为进程池中的每个进程一遍又一遍地重新执行这段代码将是低效的。或者考虑 power
是一个非常大的数组的情况。在每个子进程的内存中创建这个数组的实例可能成本太高 space。那么需要的是共享内存中数组的单个实例。
有一种机制可以在创建时使用 initializer 和 initargs 参数为池中的每个子进程初始化全局变量Pool
实例。我还利用您 是 使用自己的块大小计算的事实进行了额外的更改以节省内存:
from multiprocessing import Pool
import time
def init_pool(the_power):
""" Initialize each process's global variable power. """
global power
power = the_power
def raise_to_power(number):
number = number ** power
return number
if __name__ == '__main__':
start_time = time.time()
threads = 8
power = 10
FROM = 1
TO = 1000000
cnt = TO - FROM
# changed to a generator expression to save memory:
#numbers = (x for x in range(FROM, TO))
# or just:
numbers = range(FROM, TO)
chunksize, rem = divmod(cnt, threads)
if rem: chunksize += 1
# initialize each pool process with power:
with Pool(threads, initializer=init_pool, initargs=(power,)) as p:
Product = p.map(raise_to_power,numbers,chunksize)
print(time.time() - start_time)
进一步说明
在使用 OS 调用 spawn 而非 fork 来创建新进程的平台上创建子进程时,新进程不会继承已在父进程。相反,执行从程序的顶部开始。但是因为你 if __name__ == '__main__':
保护了创建新进程和初始化 power 的代码(这是避免递归循环无限地重新创建新进程所必需的),语句 power = 10
永远不会在子流程和 voilà。当您将 power = 10
移到 if __name__ == '__main__':
之外时,即使创建了子流程,它也会被执行。