Skip to content

Multiprocessing code succeeds in 3.13 but fails in 3.14 #128145

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

Closed
nickdrozd opened this issue Dec 21, 2024 · 6 comments
Closed

Multiprocessing code succeeds in 3.13 but fails in 3.14 #128145

nickdrozd opened this issue Dec 21, 2024 · 6 comments
Labels
type-bug An unexpected behavior, bug, or error

Comments

@nickdrozd
Copy link
Contributor

nickdrozd commented Dec 21, 2024

Bug report

Bug description:

The following code runs successfully in 3.13.1 (and earlier versions) but fails in 3.14.0a3+ (5a584c8):

from __future__ import annotations

from unittest import TestCase
from multiprocessing import Manager, Pool

LETTERS: dict[str, int]

def add_letter(letter: str) -> None:
    LETTERS[letter] += 1

class TestAsdf(TestCase):
    def setUp(self):
        global LETTERS

        LETTERS = Manager().dict(
            a = 0,
            s = 0,
            d = 0,
            f = 0,
        )

    def test_asdf(self):
        with Pool() as pool:
            pool.map(
                add_letter,
                list('asdf'))

        print(LETTERS)

Run successfully, it produces this output:

$ python3.11 -m unittest asdf

{'a': 1, 's': 1, 'd': 1, 'f': 1}

In 3.14, it fails with NameError:

$ python3.14 -m unittest asdf

======================================================================
ERROR: test_asdf (asdf.TestAsdf.test_asdf)
----------------------------------------------------------------------
multiprocessing.pool.RemoteTraceback:
"""
Traceback (most recent call last):
  File "/usr/local/lib/python3.14/multiprocessing/pool.py", line 125, in worker
    result = (True, func(*args, **kwds))
                    ~~~~^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.14/multiprocessing/pool.py", line 48, in mapstar
    return list(map(*args))
  File "/home/nick/asdf.py", line 9, in add_letter
    LETTERS[letter] += 1
    ^^^^^^^
NameError: name 'LETTERS' is not defined
"""

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/nick/asdf.py", line 24, in test_asdf
    pool.map(
    ~~~~~~~~^
        add_letter,
        ^^^^^^^^^^^
        list('asdf'))
        ^^^^^^^^^^^^^
  File "/usr/local/lib/python3.14/multiprocessing/pool.py", line 367, in map
    return self._map_async(func, iterable, mapstar, chunksize).get()
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/usr/local/lib/python3.14/multiprocessing/pool.py", line 774, in get
    raise self._value
NameError: name 'LETTERS' is not defined

This example looks stupid, but it is based on real-world test code.

CPython versions tested on:

3.11, 3.13, 3.14, CPython main branch

Operating systems tested on:

Linux

@nickdrozd nickdrozd added the type-bug An unexpected behavior, bug, or error label Dec 21, 2024
@JelleZijlstra
Copy link
Member

I believe this is because of a change in the default start method on Linux. See #125714 (comment) for a fuller explanation.

@JelleZijlstra JelleZijlstra closed this as not planned Won't fix, can't repro, duplicate, stale Dec 21, 2024
@nickdrozd
Copy link
Contributor Author

I don't understand how that relates to this code. How should this code be updated to work in 3.14?

@picnixz
Copy link
Member

picnixz commented Dec 21, 2024

I don't understand how that relates to this code.

Global objects are not picked the same way they are picked now.

How should this code be updated to work in 3.14?

I believe using multiprocessing.set_start_method('fork') should be sufficient but I haven't tested the solution (we changed the default start method from 'fork' to 'forkserver' on Linux)

@nickdrozd
Copy link
Contributor Author

Thanks, set_start_method('fork') is the solution here.

It would be good to make it explicit in the "What's new" page that this can be a breaking change on Linux and that set_start_method('fork') retains the old behavior. Currently Linux is only obliquely mentioned as "platforms other than macOS & Windows", and set_start_method is mentioned but without the critical 'fork' arg.

#84559 (comment) predicts "people requiring "fork" will have an unhappy surprise to debug in 3.14" -- that time is now upon us.

@picnixz
Copy link
Member

picnixz commented Dec 21, 2024

I can think of something for improving the changelog. You're right in saying that this is a breaking change. I also think we should highligh it in the final release notes (cc @hugovk as the RM)

@CypherGrue
Copy link

Simpler test case for this

from multiprocessing import Process, set_start_method

g = None

def process():
    global g
    print ('forked', g)

if __name__ == '__main__':
    g = 1
    print ('main', g)

    #set_start_method('fork')
    Process(target=process).start()

3.9: forked 1
3.12: forked 1
3.13: forked 1
3.14: forked None
3.14 with set_start_method('fork'): forked 1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

No branches or pull requests

4 participants