我正在使用dask或joblib将一些串行处理的python作业转换为多处理。可悲的是,我需要在窗户上工作
当从IPython或命令行运行时,用python调用py文件,一切运行正常
使用cython编译可执行文件时,它不再运行正常:逐步执行越来越多的进程(无限且大于请求的进程数)获取startet并阻止我的系统。
它在某种程度上感觉像Multiprocessing Bomb - 但当然我使用if __name__=="__main__:"
来获得控制块 - 在命令行中通过python调用进行了良好的运行。
我的cython调用是cython --embed --verbose --annotate THECODE.PY
,我正在使用gcc -time -municode -DMS_WIN64 -mthreads -Wall -O -I"PATH_TO_\include" -L"PATH_TO_\libs" THECODE.c -lpython36 -o THECODE
进行编译,导致Windows可执行文件THECODE.exe
。
使用其他(单个处理)代码运行正常
对于dask和joblib来说问题似乎是相同的(可能意味着,dask的工作方式类似于或基于joblib)。
有什么建议吗?
对于那些对mcve感兴趣的人:只需从Multiprocessing Bomb获取第一个代码并使用上面的cython命令进行编译就会导致系统崩溃。 (我刚试过:-))
我刚刚在代码示例中添加了一行来展示__name__
:
import multiprocessing
def worker():
"""worker function"""
print('Worker')
return
print("-->" + __name__ + "<--")
if __name__ == '__main__':
jobs = []
for i in range(5):
p = multiprocessing.Process(target=worker)
jobs.append(p)
p.start()
使用python
运行该段代码时,会显示
__main__
__mp_main__
__mp_main__
__mp_main__
__mp_main__
__mp_main__
(其他输出被压制)。解释if决定是否有效。 在cython和编译之后运行可执行文件时显示
__main__
__main__
__main__
__main__
__main__
__main__
越来越多。因此,对模块的工作者调用不再像导入那样masqueraded
,因此每个工作者都试图以递归方式启动五个新模块。
答案 0 :(得分:3)
起初我很惊讶地看到,你的cython版本以某种方式工作,但它只是一种工作的外观。但是,通过一些黑客行为,似乎可以使其发挥作用。
我在linux上,所以我使用/Users/koe/.terraform-versions/terraform-0.9.11/terraform init -reconfigure
Initializing the backend...
Do you want to copy state from "local" to "s3"?
Pre-existing state was found in "local" while migrating to "s3". No existing
state was found in "s3". Do you want to copy the state from "local" to
"s3"? Enter "yes" to copy and "no" to start with an empty state.
Enter a value:
来模拟windows的行为。
mp.set_start_method('spawn')
- 模式会发生什么?让我们添加一些spawn
,以便我们调查过程:
sleep
通过使用#bomb.py
import multiprocessing as mp
import sys
import time
def worker():
time.sleep(50)
print('Worker')
return
if __name__ == '__main__':
print("Starting...")
time.sleep(20)
mp.set_start_method('spawn') ## use spawn!
jobs = []
for i in range(5):
p = mp.Process(target=worker)
jobs.append(p)
p.start()
我们可以看到,起初只有一个python进程,然后是7个(!)不同的pgrep python
s。我们可以通过pid
看到命令行参数。 5个新进程有命令行
cat /proc/<pid>/cmdline
和一个:
-c "from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=5, pipe_handle=11)" --multiprocessing-fork
这意味着,父进程启动了6个新的python解释器实例,每个新启动的解释器执行从父命令通过命令行选项发送的代码,信息通过管道共享。这6个python实例中的一个是跟踪器,它可以观察整个事物。
好的,如果cythonized +嵌入会发生什么?与普通的python一样,唯一的区别是-c "from multiprocessing.semaphore_tracker import main;main(4)"
- 可执行文件是启动而不是python。但与python-interpreter不同,它不会执行/不知道命令行参数,因此bomb
函数会一遍又一遍地运行。
有一个简单的解决方法:让main
- exe启动python解释器
bomb
现在 ...
if __name__ == '__main__':
mp.set_executable(<PATH TO PYTHON>)
....
不再是多处理炸弹!
但是,目标可能不是有一个python-interpreter,所以我们需要让我们的程序知道可能的命令行:
bomb
现在,我们的炸弹不需要一个独立的python-interpreter,并在工人完成后停止。请注意以下事项:
import re
......
if __name__ == '__main__':
if len(sys.argv)==3: # should start in semaphore_tracker mode
nr=list(map(int, re.findall(r'\d+',sys.argv[2])))
sys.argv[1]='--multiprocessing-fork' # this canary is needed for multiprocessing module to work
from multiprocessing.semaphore_tracker import main;main(nr[0])
elif len(sys.argv)>3: # should start in slave mode
fd, pipe=map(int, re.findall(r'\d+',sys.argv[2]))
print("I'm a slave!, fd=%d, pipe=%d"%(fd,pipe))
sys.argv[1]='--multiprocessing-fork' # this canary is needed for multiprocessing module to work
from multiprocessing.spawn import spawn_main;
spawn_main(tracker_fd=fd, pipe_handle=pipe)
else: #main mode
print("Starting...")
mp.set_start_method('spawn')
jobs = []
for i in range(5):
p = mp.Process(target=worker)
jobs.append(p)
p.start()
不是非常错误安全的,但我希望你能得到主旨bomb
只是一只金丝雀,它不会做任何事情只有它必须在那里,请参阅here。我想以免责声明结束:我对多处理模块没有太多经验,在Windows上也没有,所以我不确定应该推荐这个解决方案。但至少它是一个有趣的:)
注意:更改的代码也可以与python一起使用,因为在执行--multiprocessing-fork
python后更改了"from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=5, pipe_handle=11)" --multiprocessing-fork
所以代码不再看到原始命令行而sys.argv
是{{ 1}}。
答案 1 :(得分:1)
受ead答案(或其中给出的想法)的启发,我找到了一个非常简单的解决方案 - 或者让我们更好地称之为解决方法。
对我来说只需将if子句更改为
if __name__ == '__main__':
if len(sys.argv) == 1:
main()
else:
sys.argv[1] = sys.argv[3]
exec(sys.argv[2])
做到了。
这有效的原因是(在我的情况下):
调用原始.py文件时,工作人员__name__
设置为__mp_main__
(但所有进程都只是普通的.py文件)。
当运行(cython)编译版本时,工作人员name
不可用,但工作人员被调用不同,因此我们可以通过argv中的更多一个参数来识别它们。在我看来,工人的argv读取
['MYPROGRAMM.exe',
'-c',
'from multiprocessing.spawn import spawn_main;
spawn_main(parent_pid=9316, pipe_handle =392)',
'--multiprocessing-fork']
因此在argv[2]
中找到了激活工人的代码,并使用上面的命令执行
当然,如果您需要编译文件的参数,则需要更大的努力,可能需要解析调用中的parent_pid。但就我而言,那只会过头了。
答案 2 :(得分:1)
我认为根据submitted bug report的细节,我可以在这里提供最优雅的解决方案
if __name__ == '__main__':
if sys.argv[0][-4:] == '.exe':
setattr(sys, 'frozen', True)
multiprocessing.freeze_support()
YOURMAINROUTINE()
Windows上需要freeze_support()
- 来电 - 请参阅python multiprocessing documentation
如果只在python中使用该行运行它已经很好了
但不知何故,cython显然不了解其中的一些内容(文档告诉它使用py2exe
,PyInstaller
和cx_Freeze
进行测试。它可以通过setattr
调用来缓解,调用只能在编译时使用,因此可以通过文件扩展来决定。
答案 3 :(得分:0)
由于建议的解决方案对我不起作用,因此我提供了另一种解决方法。
我的冻结应用程序还导致了多处理炸弹。我可以通过
解决Parallel(n_jobs=4, prefer="threads")
并行执行(而不是默认的prefer="multiprocessing")
我无法在冻结的应用程序中使用multiprocessing.Pool
(既不能使用prefer="threads"
也不能使用prefer="multiprocessing")
,但是我可以通过see docs切换到基于线程的多处理:
# a dependency with joblib
from dep_with_joblib import BigJob
# multiprocessing wrapper for threaded.Thread
from multiprocessing.dummy import Pool as ThreadPool
# instead of
# from multiprocessing import Pool
# thread based parallelism,
# works if `Parallel(n_jobs=4, prefer="threads")` is used
# in joblib (e.g. inside big_job())
POOL = ThreadPool(processes=1)
# as far as I can tell,
# the following Process based Parallelism
# does _not_ work with frozen app/joblib atm
# POOL = Pool(processes=1)
class MainClass():
def __init__(self):
"""Init ClusterGen"""
return
@staticmethod
def run_big_job(big_job, data):
"""Run big_job on parallel thread"""
big_job()
return big_job
def big_job_exec(self):
"""Big job execution"""
bigjob = BigJob()
big_job_input_data = ...
# Start big_job on different thread
async_result = POOL.apply_async(
MainClass.run_big_job, (bigjob, big_job_input_data))
# get results from clusterer
bigjob_results = async_result.get()
使用Queue
和threading.Thread
的更明确的示例:
import threading
import queue
# a dependency with joblib
from dep_with_joblib import BigJob
job_queue = queue.Queue()
def store_in_queue(f):
def wrapper(*args):
job_queue.put(f(*args))
return wrapper
class MainClass():
def __init__(self):
"""Init ClusterGen"""
return
@staticmethod
@store_in_queue
def run_big_job(big_job, data):
"""Run big_job on parallel thread"""
big_job()
return big_job
def big_job_exec(self):
"""Big job execution"""
bigjob = BigJob()
big_job_input_data = ...
# Start big_job on different thread
t = threading.Thread(
target=MainClass.run_big_job,
args=(bigjob, big_job_input_data),
group=None,
name="example-bigjob",
)
t.start()
# get results from big_job
bigjob_results = job_queue.get()
在以上两个示例中,bigjob()
在另一个线程上异步运行。可以使用多个线程轻松修改示例。
为什么异步?在我的情况下,BigJob()
是依赖项中的一个模块,使用Joblib.Parallel
来提高速度,当我的应用被冻结时,该模块将无法正常工作,并且我需要bigjob()
来异步运行以防止GUI崩溃。