Skip to content

Commit

Permalink
hooks: fix #2382 regression / improve tests and docs
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelotduarte committed Jun 9, 2024
1 parent db0a482 commit 93bc1e7
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 38 deletions.
69 changes: 59 additions & 10 deletions cx_Freeze/hooks/multiprocessing.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,65 @@ def load_multiprocessing(finder: ModuleFinder, module: Module) -> None:
sys.exit()
# workaround: inject freeze_support call to avoid an infinite loop
from multiprocessing.spawn import freeze_support as _spawn_freeze_support
from multiprocessing.context import BaseContext
BaseContext._get_context = BaseContext.get_context
def _get_freeze_context(self, method=None):
ctx = self._get_context(method)
_spawn_freeze_support()
return ctx
BaseContext.get_context = \
lambda self, method=None: _get_freeze_context(self, method)
# disable freeze_support, because it cannot be run twice
BaseContext.freeze_support = lambda self: None
from multiprocessing.spawn import is_forking as _spawn_is_forking
from multiprocessing.context import BaseContext, DefaultContext
BaseContext.freeze_support = lambda self: _spawn_freeze_support()
DefaultContext.freeze_support = lambda self: _spawn_freeze_support()
if _spawn_is_forking(sys.argv):
main_module = sys.modules["__main__"]
main_spec = main_module.__spec__
main_code = main_spec.loader.get_code(main_spec.name)
if "freeze_support" not in main_code.co_names:
print('''
An attempt has been made to start a new process before the
current process has finished its bootstrapping phase.
This probably means that you are not using fork to start your
child processes and you have forgotten to use the proper idiom
in the main module:
if __name__ == "__main__":
freeze_support()
...
To fix this issue, refer to the documentation:\n \
https://cx-freeze.readthedocs.io/en/stable/faq.html#multiprocessing-support
''', file=sys.stderr)
#import os, signal
#os.kill(os.getppid(), signal.SIGHUP)
#sys.exit(os.EX_SOFTWARE)
_spawn_freeze_support()
del main_module, main_spec, main_code
# cx_Freeze patch end
"""
code_string = module.file.read_text(encoding="utf_8") + dedent(source)
module.code = compile(
code_string,
module.file.as_posix(),
"exec",
dont_inherit=True,
optimize=finder.optimize,
)


def load_multiprocessing_context(finder: ModuleFinder, module: Module) -> None:
"""Monkeypath get_context to do automatic freeze_support."""
if IS_MINGW or IS_WINDOWS:
return
if module.file.suffix == ".pyc": # source unavailable
return
source = r"""
# cx_Freeze patch start
BaseContext._get_base_context = BaseContext.get_context
def _get_base_context(self, method=None):
self.freeze_support()
return self._get_base_context(method)
BaseContext.get_context = _get_base_context
DefaultContext._get_default_context = DefaultContext.get_context
def _get_default_context(self, method=None):
self.freeze_support()
return self._get_default_context(method)
DefaultContext.get_context = _get_default_context
# cx_Freeze patch end
"""
code_string = module.file.read_text(encoding="utf_8") + dedent(source)
Expand Down
32 changes: 21 additions & 11 deletions doc/src/faq.rst
Original file line number Diff line number Diff line change
Expand Up @@ -206,14 +206,18 @@ sources.
Multiprocessing support
-----------------------

On Linux and macOS, multiprocessing support is automatically managed by
cx_Freeze, including supporting it in pyTorch.
On Linux, macOS, and Windows, :pythondocs:`multiprocessing
<library/multiprocessing.html>` support is managed by **cx_Freeze**,
including support for PyTorch.

However, to produce a Windows executable, you must use
Depending on the platform, multiprocessing supports three ways to start a
process. These start methods are: spawn, fork, and forkserver.

However, to produce an executable, you must use
`multiprocessing.freeze_support()`.

One needs to call this function straight after the if __name__ == '__main__'
line of the main module. For example:
One needs to call this function straight after the
``if __name__ == "__main__"`` line of the main module. For example:

.. code-block:: python
Expand All @@ -228,10 +232,16 @@ line of the main module. For example:
freeze_support()
Process(target=f).start()
If the freeze_support() line is omitted then trying to run the frozen
executable will raise RuntimeError.
If the `freeze_support()` line is omitted, then running the frozen executable
will raise RuntimeError on Windows. On Linux and macOS a similar message is
shown but cx_Freeze tries to run the program by injecting a `freeze_support`.
In addition, if the module is being run normally by the Python interpreter on
any OS (the program has not been frozen), then `freeze_support()` has no
effect.

.. note::

Calling freeze_support() has no effect when invoked on any operating system
other than Windows. In addition, if the module is being run normally by the
Python interpreter on Windows (the program has not been frozen), then
freeze_support() has no effect.
Contrary to what the Python docs may state, you MUST use
`multiprocessing.freeze_support()` on Linux, macOS, and Windows.
On Linux and macOS, cx_Freeze patches the call to also handle
`multiprocessing.spawn.freeze_support()` when needed.
35 changes: 18 additions & 17 deletions tests/test_multiprocessing.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,40 +21,37 @@

SOURCE = """\
sample1.py
import multiprocessing, sys
import multiprocessing
def foo(q):
q.put('hello')
q.put("Hello from cx_Freeze")
if __name__ == '__main__':
if sys.platform == 'win32': # the conditional is unecessary
multiprocessing.freeze_support()
if __name__ == "__main__":
multiprocessing.freeze_support()
multiprocessing.set_start_method('spawn')
q = multiprocessing.SimpleQueue()
p = multiprocessing.Process(target=foo, args=(q,))
p.start()
print(q.get())
p.join()
sample2.py
import multiprocessing, sys
import multiprocessing
def foo(q):
q.put('hello')
q.put("Hello from cx_Freeze")
if __name__ == '__main__':
if __name__ == "__main__":
ctx = multiprocessing.get_context('spawn')
if sys.platform == 'win32': # the conditional is unecessary
ctx.freeze_support()
ctx.freeze_support()
q = ctx.Queue()
p = ctx.Process(target=foo, args=(q,))
p.start()
print(q.get())
p.join()
sample3.py
if __name__ == "__main__":
if __name__ == "__main__":
import multiprocessing, sys
if sys.platform == 'win32': # the conditional is unecessary
multiprocessing.freeze_support()
multiprocessing.freeze_support()
multiprocessing.set_start_method('spawn')
mgr = multiprocessing.Manager()
var = [1] * 10000000
Expand All @@ -80,18 +77,22 @@ def foo(q):
}
)
"""
EXPECTED_OUTPUT = ["hello", "hello", "creating dict...done!"]
EXPECTED_OUTPUT = [
"Hello from cx_Freeze",
"Hello from cx_Freeze",
"creating dict...done!",
]


def _parameters_data() -> Iterator:
methods = mp.get_all_start_methods()
for method in methods:
source = SOURCE.replace("('spawn')", f"('{method}')")
for i, expected in enumerate(EXPECTED_OUTPUT):
for i, expected in enumerate(EXPECTED_OUTPUT, 1):
if method == "forkserver" and i != 3:
continue # only sample3 works with forkserver method
sample = f"sample{i+1}"
test_id = f"{sample},{method}"
sample = f"sample{i}"
test_id = f"{sample}-{method}"
yield pytest.param(source, sample, expected, id=test_id)


Expand Down

0 comments on commit 93bc1e7

Please sign in to comment.