Skip to content

Commit 9a76735

Browse files
committed
Close #19946: use runpy as needed in multiprocessing
- handles main files without a suffix - handles main submodules properly - adds test cases for the various kinds of __main__
1 parent 7cff4cd commit 9a76735

File tree

4 files changed

+375
-54
lines changed

4 files changed

+375
-54
lines changed

Doc/whatsnew/3.4.rst

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -624,13 +624,22 @@ mmap objects can now be weakref'ed.
624624
multiprocessing
625625
---------------
626626

627-
On Unix two new *start methods* have been added for starting processes
628-
using :mod:`multiprocessing`. These make the mixing of processes with
629-
threads more robust. See :issue:`8713`.
627+
On Unix, two new *start methods* (``spawn`` and ``forkserver``) have been
628+
added for starting processes using :mod:`multiprocessing`. These make
629+
the mixing of processes with threads more robust, and the ``spawn``
630+
method matches the semantics that multiprocessing has always used on
631+
Windows. (Contributed by Richard Oudkerk in :issue:`8713`).
630632

631633
Also, except when using the old *fork* start method, child processes
632634
will no longer inherit unneeded handles/file descriptors from their parents.
633635

636+
:mod:`multiprocessing` now relies on :mod:`runpy` (which implements the
637+
``-m`` switch) to initialise ``__main__`` appropriately in child processes
638+
when using the ``spawn`` or ``forkserver`` start methods. This resolves some
639+
edge cases where combining multiprocessing, the ``-m`` command line switch
640+
and explicit relative imports could cause obscure failures in child
641+
processes. (Contributed by Nick Coghlan in :issue:`19946`)
642+
634643

635644
os
636645
--

Lib/multiprocessing/spawn.py

Lines changed: 70 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import os
1212
import pickle
1313
import sys
14+
import runpy
15+
import types
1416

1517
from . import get_start_method, set_start_method
1618
from . import process
@@ -157,15 +159,19 @@ def get_preparation_data(name):
157159
start_method=get_start_method(),
158160
)
159161

160-
if sys.platform != 'win32' or (not WINEXE and not WINSERVICE):
161-
main_path = getattr(sys.modules['__main__'], '__file__', None)
162-
if not main_path and sys.argv[0] not in ('', '-c'):
163-
main_path = sys.argv[0]
162+
# Figure out whether to initialise main in the subprocess as a module
163+
# or through direct execution (or to leave it alone entirely)
164+
main_module = sys.modules['__main__']
165+
main_mod_name = getattr(main_module.__spec__, "name", None)
166+
if main_mod_name is not None:
167+
d['init_main_from_name'] = main_mod_name
168+
elif sys.platform != 'win32' or (not WINEXE and not WINSERVICE):
169+
main_path = getattr(main_module, '__file__', None)
164170
if main_path is not None:
165171
if (not os.path.isabs(main_path) and
166172
process.ORIGINAL_DIR is not None):
167173
main_path = os.path.join(process.ORIGINAL_DIR, main_path)
168-
d['main_path'] = os.path.normpath(main_path)
174+
d['init_main_from_path'] = os.path.normpath(main_path)
169175

170176
return d
171177

@@ -206,55 +212,68 @@ def prepare(data):
206212
if 'start_method' in data:
207213
set_start_method(data['start_method'])
208214

209-
if 'main_path' in data:
210-
import_main_path(data['main_path'])
215+
if 'init_main_from_name' in data:
216+
_fixup_main_from_name(data['init_main_from_name'])
217+
elif 'init_main_from_path' in data:
218+
_fixup_main_from_path(data['init_main_from_path'])
219+
220+
# Multiprocessing module helpers to fix up the main module in
221+
# spawned subprocesses
222+
def _fixup_main_from_name(mod_name):
223+
# __main__.py files for packages, directories, zip archives, etc, run
224+
# their "main only" code unconditionally, so we don't even try to
225+
# populate anything in __main__, nor do we make any changes to
226+
# __main__ attributes
227+
current_main = sys.modules['__main__']
228+
if mod_name == "__main__" or mod_name.endswith(".__main__"):
229+
return
230+
231+
# If this process was forked, __main__ may already be populated
232+
if getattr(current_main.__spec__, "name", None) == mod_name:
233+
return
234+
235+
# Otherwise, __main__ may contain some non-main code where we need to
236+
# support unpickling it properly. We rerun it as __mp_main__ and make
237+
# the normal __main__ an alias to that
238+
old_main_modules.append(current_main)
239+
main_module = types.ModuleType("__mp_main__")
240+
main_content = runpy.run_module(mod_name,
241+
run_name="__mp_main__",
242+
alter_sys=True)
243+
main_module.__dict__.update(main_content)
244+
sys.modules['__main__'] = sys.modules['__mp_main__'] = main_module
245+
246+
247+
def _fixup_main_from_path(main_path):
248+
# If this process was forked, __main__ may already be populated
249+
current_main = sys.modules['__main__']
250+
251+
# Unfortunately, the main ipython launch script historically had no
252+
# "if __name__ == '__main__'" guard, so we work around that
253+
# by treating it like a __main__.py file
254+
# See https://github.com/ipython/ipython/issues/4698
255+
main_name = os.path.splitext(os.path.basename(main_path))[0]
256+
if main_name == 'ipython':
257+
return
258+
259+
# Otherwise, if __file__ already has the setting we expect,
260+
# there's nothing more to do
261+
if getattr(current_main, '__file__', None) == main_path:
262+
return
263+
264+
# If the parent process has sent a path through rather than a module
265+
# name we assume it is an executable script that may contain
266+
# non-main code that needs to be executed
267+
old_main_modules.append(current_main)
268+
main_module = types.ModuleType("__mp_main__")
269+
main_content = runpy.run_path(main_path,
270+
run_name="__mp_main__")
271+
main_module.__dict__.update(main_content)
272+
sys.modules['__main__'] = sys.modules['__mp_main__'] = main_module
211273

212274

213275
def import_main_path(main_path):
214276
'''
215277
Set sys.modules['__main__'] to module at main_path
216278
'''
217-
# XXX (ncoghlan): The following code makes several bogus
218-
# assumptions regarding the relationship between __file__
219-
# and a module's real name. See PEP 302 and issue #10845
220-
if getattr(sys.modules['__main__'], '__file__', None) == main_path:
221-
return
222-
223-
main_name = os.path.splitext(os.path.basename(main_path))[0]
224-
if main_name == '__init__':
225-
main_name = os.path.basename(os.path.dirname(main_path))
226-
227-
if main_name == '__main__':
228-
main_module = sys.modules['__main__']
229-
main_module.__file__ = main_path
230-
elif main_name != 'ipython':
231-
# Main modules not actually called __main__.py may
232-
# contain additional code that should still be executed
233-
import importlib
234-
import types
235-
236-
if main_path is None:
237-
dirs = None
238-
elif os.path.basename(main_path).startswith('__init__.py'):
239-
dirs = [os.path.dirname(os.path.dirname(main_path))]
240-
else:
241-
dirs = [os.path.dirname(main_path)]
242-
243-
assert main_name not in sys.modules, main_name
244-
sys.modules.pop('__mp_main__', None)
245-
# We should not try to load __main__
246-
# since that would execute 'if __name__ == "__main__"'
247-
# clauses, potentially causing a psuedo fork bomb.
248-
main_module = types.ModuleType(main_name)
249-
# XXX Use a target of main_module?
250-
spec = importlib.find_spec(main_name, path=dirs)
251-
if spec is None:
252-
raise ImportError(name=main_name)
253-
methods = importlib._bootstrap._SpecMethods(spec)
254-
methods.init_module_attrs(main_module)
255-
main_module.__name__ = '__mp_main__'
256-
code = spec.loader.get_code(main_name)
257-
exec(code, main_module.__dict__)
258-
259-
old_main_modules.append(sys.modules['__main__'])
260-
sys.modules['__main__'] = sys.modules['__mp_main__'] = main_module
279+
_fixup_main_from_path(main_path)

0 commit comments

Comments
 (0)