问题
I wrote a Python class to plot pylots in parallel. It works fine on Linux where the default start method is fork but when I tried it on Windows I ran into problems (which can be reproduced on Linux using the spawn start method - see code below). I always end up getting this error:
Traceback (most recent call last):
File "test.py", line 50, in <module>
test()
File "test.py", line 7, in test
asyncPlotter.saveLinePlotVec3("test")
File "test.py", line 41, in saveLinePlotVec3
args=(test, ))
File "test.py", line 34, in process
p.start()
File "C:\Users\adrian\AppData\Local\Programs\Python\Python37\lib\multiprocessing\process.py", line 112, in start
self._popen = self._Popen(self)
File "C:\Users\adrian\AppData\Local\Programs\Python\Python37\lib\multiprocessing\context.py", line 223, in _Popen
return _default_context.get_context().Process._Popen(process_obj)
File "C:\Users\adrian\AppData\Local\Programs\Python\Python37\lib\multiprocessing\context.py", line 322, in _Popen
return Popen(process_obj)
File "C:\Users\adrian\AppData\Local\Programs\Python\Python37\lib\multiprocessing\popen_spawn_win32.py", line 89, in __init__
reduction.dump(process_obj, to_child)
File "C:\Users\adrian\AppData\Local\Programs\Python\Python37\lib\multiprocessing\reduction.py", line 60, in dump
ForkingPickler(file, protocol).dump(obj)
TypeError: can't pickle weakref objects
C:\Python\MonteCarloTools>Traceback (most recent call last):
File "<string>", line 1, in <module>
File "C:\Users\adrian\AppData\Local\Programs\Python\Python37\lib\multiprocessing\spawn.py", line 99, in spawn_main
new_handle = reduction.steal_handle(parent_pid, pipe_handle)
File "C:\Users\adrian\AppData\Local\Programs\Python\Python37\lib\multiprocessing\reduction.py", line 82, in steal_handle
_winapi.PROCESS_DUP_HANDLE, False, source_pid)
OSError: [WinError 87] The parameter is incorrect
I hope there is a way to make this code work for Windows. Here a link to the different start methods available on Linux and Windows: https://docs.python.org/3/library/multiprocessing.html#contexts-and-start-methods
import multiprocessing as mp
def test():
manager = mp.Manager()
asyncPlotter = AsyncPlotter(manager.Value('i', 0))
asyncPlotter.saveLinePlotVec3("test")
asyncPlotter.saveLinePlotVec3("test")
asyncPlotter.join()
class AsyncPlotter():
def __init__(self, nc, processes=mp.cpu_count()):
self.nc = nc
self.pids = []
self.processes = processes
def linePlotVec3(self, nc, processes, test):
self.waitOnPool(nc, processes)
print(test)
nc.value -= 1
def waitOnPool(self, nc, processes):
while nc.value >= processes:
time.sleep(0.1)
nc.value += 1
def process(self, target, args):
ctx = mp.get_context('spawn')
p = ctx.Process(target=target, args=args)
p.start()
self.pids.append(p)
def saveLinePlotVec3(self, test):
self.process(target=self.linePlotVec3,
args=(self.nc, self.processes, test))
def join(self):
for p in self.pids:
p.join()
if __name__=='__main__':
test()
回答1:
When using the spawn
start method, the Process
object itself is being pickled for use in the child process. In your code, the target=target
argument is a bound method of AsyncPlotter
. It looks like the entire asyncPlotter
instance must also be pickled for that to work, and that includes self.manager
, which apparently doesn't want to be pickled.
In short, keep Manager
outside of AsyncPlotter
. This works on my macOS system:
def test():
manager = mp.Manager()
asyncPlotter = AsyncPlotter(manager.Value('i', 0))
...
Also, as noted in your comment, asyncPlotter
did not work when reused. I don't know the details but looks like it has something to do with how the Value
object is shared across processes. The test
function would need to be like:
def test():
manager = mp.Manager()
nc = manager.Value('i', 0)
asyncPlotter1 = AsyncPlotter(nc)
asyncPlotter1.saveLinePlotVec3("test 1")
asyncPlotter2 = AsyncPlotter(nc)
asyncPlotter2.saveLinePlotVec3("test 2")
asyncPlotter1.join()
asyncPlotter2.join()
All in all, you might want to restructure your code and use a process pool. It already handles what AsyncPlotter
is doing with cpu_count
and parallel execution:
from multiprocessing import Pool, set_start_method
from random import random
import time
def linePlotVec3(test):
time.sleep(random())
print("test", test)
if __name__ == "__main__":
set_start_method("spawn")
with Pool() as pool:
pool.map(linePlotVec3, range(20))
Or you could use a ProcessPoolExecutor to do pretty much the same thing. This example starts tasks one at a time instead of mapping to a list:
from concurrent.futures import ProcessPoolExecutor
import multiprocessing as mp
import time
from random import random
def work(i):
r = random()
print("work", i, r)
time.sleep(r)
def main():
ctx = mp.get_context("spawn")
with ProcessPoolExecutor(mp_context=ctx) as pool:
for i in range(20):
pool.submit(work, i)
if __name__ == "__main__":
main()
回答2:
For portability, all objects passed as arguments to a function that will be run in a process must be picklable.
来源:https://stackoverflow.com/questions/57191393/python-multiprocessing-with-start-method-spawn-doesnt-work