Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiprocessing Manager Class Causes Infinite Loop on Mac OS (intel) #2435

Closed
IsaiahCoroama opened this issue Jun 3, 2024 · 18 comments · Fixed by #2443
Closed

Multiprocessing Manager Class Causes Infinite Loop on Mac OS (intel) #2435

IsaiahCoroama opened this issue Jun 3, 2024 · 18 comments · Fixed by #2443

Comments

@IsaiahCoroama
Copy link

IsaiahCoroama commented Jun 3, 2024

Bug Description
When I try to create a multiprocessing.Manager() object, even with freeze_support, it causes an infinite loop.

Steps To Reproduce
Create a main.py file.

is_darwin = True # This is determined by a module that does not affect the results.

def main():
    ...

if __name__ == "__main__":
    if is_darwin:
       import multiprocessing; multiprocessing.freeze_support()
        
       print("Creating Managers")
       manager_1 = multiprocessing.Manager()
       print("Finished Creating Manager #1")
       manager_2 = multiprocessing.Manager()
       print("Finished Creating Manager #2")
       manager_3 = multiprocessing.Manager()
       print("Finished Creating Manager #3")
       print("Finished Creating ALL Managers.")

    main()

Create a setup.py file.

from cx_Freeze import setup, Executable

build_exe_options = {
    ["zip_include_packages"]: []
}

setup(
    name="main-demo",
    version="0.0.0",
    options={"build_exe": build_exe_options},
    executables=[Executable("main.py", base="console")]
)

Run the setup file with the following configuration:

python setup.py bdist_mac

Run the generated application file:

./build/main-demo-1.0.0.app/Contents/MacOS/main

Expected behavior
From the demo, it prints "Creating Managers" repeatedly indefinity.

System Information:

  • Platform information: Mac OS (Sonoma 14.4)
  • OS architecture: x86_64
  • cx_Freeze version: 7.1.0.post0
  • Python version: 3.12.3

Additional context
Within the project, I am using managers to share data between three separate processes. These processes run a loop attached to a manager.Event() object. In the loop, it checks if a boolean value within a manager.dict() object is True. If it is, it will call a tkinter function; these consist of filedialog.askdirectory(), messagebox.showerror(), and simpledialog.askstring(). Then, it sets the output to a value within the manager dictionary. The process is then repeated with a few other steps. If you would like, I can post the full source code, but currently, the issue only remains with creating the first manager object. I have gotten this to work with PyInstaller, but I need to create a universal2 application, and cx_Freeze has been the closest I have gotten to finally completing everything I need.

Although it states in the official documentation that multiprocessing.freeze_support() only works on Windows, it has fixed this same infinite loop within PyInstaller when I was trying to get that to work.

Disclaimer
If there are any misspellings within the code above, that is an accident and not in my actual code. I wrote this issue from my Windows machine all by hand.

@IsaiahCoroama
Copy link
Author

After looking into the source code of the hook function for the multiprocessing module, I read the comment about multiprocessing.spawn.freeze_support() working on all operating systems and tried running it as well. It fixed the issue. I'm sorry I didn't figure this out sooner and made an issue for it when there was already a fix. I at least hope that this issue helps others who may run into this problem.

is_darwin = True # This is determined by a module that does not affect the results.

def main():
    ...

if __name__ == "__main__":
    if is_darwin:
       import multiprocessing.spawn
       import multiprocessing

       multiprocessing.spawn.freeze_support() # This fixes the issue.
        
       print("Creating Managers")
       manager_1 = multiprocessing.Manager()
       print("Finished Creating Manager #1")
       manager_2 = multiprocessing.Manager()
       print("Finished Creating Manager #2")
       manager_3 = multiprocessing.Manager()
       print("Finished Creating Manager #3")
       print("Finished Creating ALL Managers.")

    main()

@marcelotduarte
Copy link
Owner

@IsaiahCoroama
Copy link
Author

I reviewed all of your test cases with the following setup.

[sample1.py]

import multiprocessing, sys

def foo(q):
    q.put('hello')

if __name__ == '__main__':
    if sys.platform == 'win32':  # the conditional is unecessary
        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

def foo(q):
    q.put('hello')

if __name__ == '__main__':
    ctx = multiprocessing.get_context('spawn')
    if sys.platform == 'win32':  # the conditional is unecessary
        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__":
    import multiprocessing, sys
    if sys.platform == 'win32':  # the conditional is unecessary
        multiprocessing.freeze_support()
    multiprocessing.set_start_method('spawn')
    mgr = multiprocessing.Manager()
    var = [1] * 10000000
    print("creating dict", end="...")
    mgr_dict = mgr.dict({'test': var})
    print("done!")

[setup.py]

from cx_Freeze import Executable, setup

setup(
    name="test_multiprocessing",
    version="0.1",
    description="Sample for test with cx_Freeze",
    executables=[
        Executable("sample1.py"),
        Executable("sample2.py"),
        Executable("sample3.py"),
    ],
    options={
        "build_exe": {
            "excludes": ["tkinter"],
            "silent": True,
        }
    }
)

[sample1.py Run Command]

./build/exe.macos-10.9-universal2-3.12/sample1
./build/test_multiprocessing-0.1.app/Contents/MacOS/sample1

[sample2.py Run Command]

./build/exe.macos-10.9-universal2-3.12/sample2
./build/test_multiprocessing-0.1.app/Contents/MacOS/sample2

[sample3.py Run Command]

./build/exe.macos-10.9-universal2-3.12/sample3
./build/test_multiprocessing-0.1.app/Contents/MacOS/sample3

[setup.py Run Command]

python setup.py bdist_mac

Output
From the following tests above, all produced the expected output. For sample 3, I created an altered test case by removing the multiprocessing.set_start_method("spawn") and was able to reproduce the infinite spawn recursion as before.

Alternate Test Case For Sample 3

if __name__ ==  "__main__":
    import multiprocessing, sys
    if sys.platform == 'win32':  # the conditional is unecessary
        multiprocessing.freeze_support()
    # multiprocessing.set_start_method('spawn') Removed for testing purposes
    print("Creating Manager")
    mgr = multiprocessing.Manager()
    var = [1] * 10000000
    print("creating dict", end="...")
    mgr_dict = mgr.dict({'test': var})
    print("done!")

[Run Command]

./build/exe.macos-10.9-universal2-3.12/sample3
./build/test_multiprocessing-0.1.app/Contents/MacOS/sample3

[Output]

user@user-ImMac multiprocessing % ./build/exe.macos-10.9-universal2-3.12/sample3
Creating Manager
Creating Manager
Creating Manager
Creating Manager
Creating Manager
...
user@user-iMac multiprocessing % ./build/test_multiprocessing-0.1.app/Contents/MacOS/sample3
Creating Manager
Creating Manager
Creating Manager
Creating Manager
Creating Manager
...

Conclusion
From the following tests, not calling either mutliprocessing.spawn.freeze_support() or multiprocessing.set_start_method("spawn") will cause the multiprocessing.Manager() to run in an infinite loop.

@marcelotduarte
Copy link
Owner

Your conclusion is interesting, because according to the Python documentation, spawn is the default start method. Then there would be no need to configure it, and in the set_start_method documentation it states "that this should be called at most once".
However based on the examples, I think that the alternative (correct?) for sample3 is:

if __name__ ==  "__main__":
    import multiprocessing
    mp_context = multiprocessing.get_context('spawn')
    print("Creating Manager")
    mgr = mp_context.Manager()
    var = [1] * 10000000
    print("creating dict", end="...")
    mgr_dict = mgr.dict({'test': var})
    print("done!")

Also, in the The spawn and forkserver start methods section has "Safe importing of main module" with recommendations to use set_start_method.

Can you check if the default start method is indeed spawn in python and in the executable?

@ntindle
Copy link
Contributor

ntindle commented Jun 4, 2024

freeze_support from spawn worked for my full use case but freeze_support from multiprocessing directly did not. How odd. I didn't try the trimmed down example, but my work is with Queue and Process

@IsaiahCoroama
Copy link
Author

IsaiahCoroama commented Jun 4, 2024

[sample4.py]

if __name__ ==  "__main__":
    import multiprocessing
    mp_context = multiprocessing.get_context('spawn')
    print("Creating Manager")
    mgr = mp_context.Manager()
    var = [1] * 10000000
    print("creating dict", end="...")
    mgr_dict = mgr.dict({'test': var})
    print("done!")

Output

user@user-ImMac multiprocessing % ./build/exe.macos-10.9-universal2-3.12/sample3
Creating Manager
creating dict...done!
user@user-iMac multiprocessing % ./build/test_multiprocessing-0.1.app/Contents/MacOS/sample3
Creating Manager
creating dict...done!

Alternate Test Case For Sample 4

if __name__ ==  "__main__":
    import multiprocessing
    #mp_context = multiprocessing.get_context('spawn') Removed for test purposes
    print("Creating Manager")
    mgr = multiprocessing.Manager()
    var = [1] * 10000000
    print("creating dict", end="...")
    mgr_dict = mgr.dict({'test': var})
    print("done!")

Output of Alternate Test Case

user@user-ImMac multiprocessing % ./build/exe.macos-10.9-universal2-3.12/sample4
Creating Manager
Creating Manager
Creating Manager
Creating Manager
Creating Manager
...
user@user-iMac multiprocessing % ./build/test_multiprocessing-0.1.app/Contents/MacOS/sample4
Creating Manager
Creating Manager
Creating Manager
Creating Manager
Creating Manager
...

The Default Start Method

if __name__ ==  "__main__":
    import multiprocessing
    print(multiprocessing.get_start_method()) # The default start method
    print("Creating Manager")
    mgr = mp_context.Manager()
    var = [1] * 10000000
    print("creating dict", end="...")
    mgr_dict = mgr.dict({'test': var})
    print("done!")

Output

user@user-ImMac multiprocessing % ./build/exe.macos-10.9-universal2-3.12/sample4
spawn
Creating Manager
spawn
Creating Manager
spawn
Creating Manager
...
user@user-iMac multiprocessing % ./build/test_multiprocessing-0.1.app/Contents/MacOS/sample4
spawn
Creating Manager
spawn
Creating Manager
spawn
Creating Manager
...

Another Test Case with Getting the Default Spawn Method

if __name__ ==  "__main__":
    import multiprocessing.spawn
    import multiprocessing
    multiprocessing.spawn.freeze_support()
    print(multiprocessing.get_start_method()) # The default start method
    print("Creating Manager")
    mgr = mp_context.Manager()
    var = [1] * 10000000
    print("creating dict", end="...")
    mgr_dict = mgr.dict({'test': var})
    print("done!")

Output

user@user-ImMac multiprocessing % ./build/exe.macos-10.9-universal2-3.12/sample4
spawn
Creating Manager
creating dict...done!
user@user-iMac multiprocessing % ./build/test_multiprocessing-0.1.app/Contents/MacOS/sample4
spawn
Creating Manager
creating dict...done!

Off Topic Question
I've noticed that "universal2" is labeled in the name of the generated build_exe directory, but it is not a standalone file. Is there a way to create standalone universal2 executable files using cx_Freeze?

@ntindle
Copy link
Contributor

ntindle commented Jun 4, 2024

Trimmed example for context:

from multiprocessing import Process, freeze_support


def f():
    print("Hello from cx_Freeze")


if __name__ == "__main__":
    freeze_support()
    Process(target=f).start()

@ntindle
Copy link
Contributor

ntindle commented Jun 4, 2024

The default in python is spawn and it works if you set it to fork. However, the notice in the FAQ could probably just be updated to mention both freeze support functions without any additional changes

@marcelotduarte
Copy link
Owner

@IsaiahCoroama read this.
For "Alternate Test Case For Sample 4" the mgr = mp_context.Manager() was changed to mgr = multiprocessing.Manager() to run?

@ntindle I should change the FAQ or redirect freeze_support in the hook to avoid future questions....

@marcelotduarte marcelotduarte reopened this Jun 4, 2024
@ntindle
Copy link
Contributor

ntindle commented Jun 4, 2024

My naive understanding is that you sometimes need one vs the other depending on if you want to spawn or fork which differs by platform

@IsaiahCoroama
Copy link
Author

@IsaiahCoroama read this. For "Alternate Test Case For Sample 4" the mgr = mp_context.Manager() was changed to mgr = multiprocessing.Manager() to run?

@ntindle I should change the FAQ or redirect freeze_support in the hook to avoid future questions....

Apologies for the confusion. You're right, it should be multiprocessing.Manager() instead of mp_context.Manager(). I'll update the post above with the correct text. Additionally, I've rerun the test, and it yielded the same output as previously mentioned.

@IsaiahCoroama

This comment was marked as off-topic.

@marcelotduarte

This comment was marked as off-topic.

@IsaiahCoroama

This comment was marked as off-topic.

@IsaiahCoroama
Copy link
Author

The conclusion above along with the data for it, is more related to #2329, that it is to the current issue. (#2435)

@marcelotduarte
Copy link
Owner

Uninstall cx_Freeze and reinstall to check the wheel installed, or download https://files.pythonhosted.org/packages/e8/12/70285b18f9dea354e69ee1b472f46e79f9ae9aa6749d23a3109bb73aa9fc/cx_Freeze-7.1.0.post0-cp312-cp312-macosx_10_9_universal2.whl and install it, so you should get a universal2 executable.
image

@marcelotduarte
Copy link
Owner

Based on information from you and others, I improved the hook for multiprocessing.
You can test the patch in the latest development build:
pip install --force --no-cache --pre --extra-index-url https://marcelotduarte.github.io/packages/ cx_Freeze
The provisional documentation: https://cx-freeze--2443.org.readthedocs.build/en/2443/faq.html#multiprocessing-support

@marcelotduarte
Copy link
Owner

Release 7.1.1 is out!
Documentation

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants