mirror of
https://github.com/python/cpython.git
synced 2025-08-09 11:29:45 +00:00
[3.12] gh-102512: Turn _DummyThread into _MainThread after os.fork() called from a foreign thread (GH-113261) (GH-114430)
Always set a _MainThread as a main thread after os.fork() is called from
a thread started not by the threading module.
A new _MainThread was already set as a new main thread after fork if
threading.current_thread() was not called for a foreign thread before fork.
Now, if it was called before fork, the implicitly created _DummyThread will
be turned into _MainThread after fork.
It fixes, in particularly, an incompatibility of _DummyThread with
the threading shutdown logic which relies on the main thread
having tstate_lock.
(cherry picked from commit 49785b06de
)
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
Co-authored-by: Marek Marczykowski-Górecki <marmarek@invisiblethingslab.com>
This commit is contained in:
parent
d1f731ff08
commit
1bd2c93474
3 changed files with 98 additions and 9 deletions
|
@ -115,6 +115,7 @@ class BaseTestCase(unittest.TestCase):
|
||||||
|
|
||||||
|
|
||||||
class ThreadTests(BaseTestCase):
|
class ThreadTests(BaseTestCase):
|
||||||
|
maxDiff = 9999
|
||||||
|
|
||||||
@cpython_only
|
@cpython_only
|
||||||
def test_name(self):
|
def test_name(self):
|
||||||
|
@ -627,19 +628,25 @@ class ThreadTests(BaseTestCase):
|
||||||
import os, threading
|
import os, threading
|
||||||
from test import support
|
from test import support
|
||||||
|
|
||||||
|
ident = threading.get_ident()
|
||||||
pid = os.fork()
|
pid = os.fork()
|
||||||
if pid == 0:
|
if pid == 0:
|
||||||
|
print("current ident", threading.get_ident() == ident)
|
||||||
main = threading.main_thread()
|
main = threading.main_thread()
|
||||||
print(main.name)
|
print("main", main.name)
|
||||||
print(main.ident == threading.current_thread().ident)
|
print("main ident", main.ident == ident)
|
||||||
print(main.ident == threading.get_ident())
|
print("current is main", threading.current_thread() is main)
|
||||||
else:
|
else:
|
||||||
support.wait_process(pid, exitcode=0)
|
support.wait_process(pid, exitcode=0)
|
||||||
"""
|
"""
|
||||||
_, out, err = assert_python_ok("-c", code)
|
_, out, err = assert_python_ok("-c", code)
|
||||||
data = out.decode().replace('\r', '')
|
data = out.decode().replace('\r', '')
|
||||||
self.assertEqual(err, b"")
|
self.assertEqual(err, b"")
|
||||||
self.assertEqual(data, "MainThread\nTrue\nTrue\n")
|
self.assertEqual(data,
|
||||||
|
"current ident True\n"
|
||||||
|
"main MainThread\n"
|
||||||
|
"main ident True\n"
|
||||||
|
"current is main True\n")
|
||||||
|
|
||||||
@skip_unless_reliable_fork
|
@skip_unless_reliable_fork
|
||||||
@unittest.skipUnless(hasattr(os, 'waitpid'), "test needs os.waitpid()")
|
@unittest.skipUnless(hasattr(os, 'waitpid'), "test needs os.waitpid()")
|
||||||
|
@ -649,15 +656,17 @@ class ThreadTests(BaseTestCase):
|
||||||
from test import support
|
from test import support
|
||||||
|
|
||||||
def func():
|
def func():
|
||||||
|
ident = threading.get_ident()
|
||||||
with warnings.catch_warnings(record=True) as ws:
|
with warnings.catch_warnings(record=True) as ws:
|
||||||
warnings.filterwarnings(
|
warnings.filterwarnings(
|
||||||
"always", category=DeprecationWarning)
|
"always", category=DeprecationWarning)
|
||||||
pid = os.fork()
|
pid = os.fork()
|
||||||
if pid == 0:
|
if pid == 0:
|
||||||
|
print("current ident", threading.get_ident() == ident)
|
||||||
main = threading.main_thread()
|
main = threading.main_thread()
|
||||||
print(main.name)
|
print("main", main.name, type(main).__name__)
|
||||||
print(main.ident == threading.current_thread().ident)
|
print("main ident", main.ident == ident)
|
||||||
print(main.ident == threading.get_ident())
|
print("current is main", threading.current_thread() is main)
|
||||||
# stdout is fully buffered because not a tty,
|
# stdout is fully buffered because not a tty,
|
||||||
# we have to flush before exit.
|
# we have to flush before exit.
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
@ -673,7 +682,80 @@ class ThreadTests(BaseTestCase):
|
||||||
_, out, err = assert_python_ok("-c", code)
|
_, out, err = assert_python_ok("-c", code)
|
||||||
data = out.decode().replace('\r', '')
|
data = out.decode().replace('\r', '')
|
||||||
self.assertEqual(err.decode('utf-8'), "")
|
self.assertEqual(err.decode('utf-8'), "")
|
||||||
self.assertEqual(data, "Thread-1 (func)\nTrue\nTrue\n")
|
self.assertEqual(data,
|
||||||
|
"current ident True\n"
|
||||||
|
"main Thread-1 (func) Thread\n"
|
||||||
|
"main ident True\n"
|
||||||
|
"current is main True\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.platform in platforms_to_skip, "due to known OS bug")
|
||||||
|
@support.requires_fork()
|
||||||
|
@unittest.skipUnless(hasattr(os, 'waitpid'), "test needs os.waitpid()")
|
||||||
|
def test_main_thread_after_fork_from_foreign_thread(self, create_dummy=False):
|
||||||
|
code = """if 1:
|
||||||
|
import os, threading, sys, traceback, _thread
|
||||||
|
from test import support
|
||||||
|
|
||||||
|
def func(lock):
|
||||||
|
ident = threading.get_ident()
|
||||||
|
if %s:
|
||||||
|
# call current_thread() before fork to allocate DummyThread
|
||||||
|
current = threading.current_thread()
|
||||||
|
print("current", current.name, type(current).__name__)
|
||||||
|
print("ident in _active", ident in threading._active)
|
||||||
|
# flush before fork, so child won't flush it again
|
||||||
|
sys.stdout.flush()
|
||||||
|
pid = os.fork()
|
||||||
|
if pid == 0:
|
||||||
|
print("current ident", threading.get_ident() == ident)
|
||||||
|
main = threading.main_thread()
|
||||||
|
print("main", main.name, type(main).__name__)
|
||||||
|
print("main ident", main.ident == ident)
|
||||||
|
print("current is main", threading.current_thread() is main)
|
||||||
|
print("_dangling", [t.name for t in list(threading._dangling)])
|
||||||
|
# stdout is fully buffered because not a tty,
|
||||||
|
# we have to flush before exit.
|
||||||
|
sys.stdout.flush()
|
||||||
|
try:
|
||||||
|
threading._shutdown()
|
||||||
|
os._exit(0)
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
|
sys.stderr.flush()
|
||||||
|
os._exit(1)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
support.wait_process(pid, exitcode=0)
|
||||||
|
except Exception:
|
||||||
|
# avoid 'could not acquire lock for
|
||||||
|
# <_io.BufferedWriter name='<stderr>'> at interpreter shutdown,'
|
||||||
|
traceback.print_exc()
|
||||||
|
sys.stderr.flush()
|
||||||
|
finally:
|
||||||
|
lock.release()
|
||||||
|
|
||||||
|
join_lock = _thread.allocate_lock()
|
||||||
|
join_lock.acquire()
|
||||||
|
th = _thread.start_new_thread(func, (join_lock,))
|
||||||
|
join_lock.acquire()
|
||||||
|
""" % create_dummy
|
||||||
|
# "DeprecationWarning: This process is multi-threaded, use of fork()
|
||||||
|
# may lead to deadlocks in the child"
|
||||||
|
_, out, err = assert_python_ok("-W", "ignore::DeprecationWarning", "-c", code)
|
||||||
|
data = out.decode().replace('\r', '')
|
||||||
|
self.assertEqual(err.decode(), "")
|
||||||
|
self.assertEqual(data,
|
||||||
|
("current Dummy-1 _DummyThread\n" if create_dummy else "") +
|
||||||
|
f"ident in _active {create_dummy!s}\n" +
|
||||||
|
"current ident True\n"
|
||||||
|
"main MainThread _MainThread\n"
|
||||||
|
"main ident True\n"
|
||||||
|
"current is main True\n"
|
||||||
|
"_dangling ['MainThread']\n")
|
||||||
|
|
||||||
|
def test_main_thread_after_fork_from_dummy_thread(self, create_dummy=False):
|
||||||
|
self.test_main_thread_after_fork_from_foreign_thread(create_dummy=True)
|
||||||
|
|
||||||
def test_main_thread_during_shutdown(self):
|
def test_main_thread_during_shutdown(self):
|
||||||
# bpo-31516: current_thread() should still point to the main thread
|
# bpo-31516: current_thread() should still point to the main thread
|
||||||
|
|
|
@ -1460,7 +1460,6 @@ class _DummyThread(Thread):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
Thread.__init__(self, name=_newname("Dummy-%d"),
|
Thread.__init__(self, name=_newname("Dummy-%d"),
|
||||||
daemon=_daemon_threads_allowed())
|
daemon=_daemon_threads_allowed())
|
||||||
|
|
||||||
self._started.set()
|
self._started.set()
|
||||||
self._set_ident()
|
self._set_ident()
|
||||||
if _HAVE_THREAD_NATIVE_ID:
|
if _HAVE_THREAD_NATIVE_ID:
|
||||||
|
@ -1685,6 +1684,11 @@ def _after_fork():
|
||||||
# its new value since it can have changed.
|
# its new value since it can have changed.
|
||||||
thread._reset_internal_locks(True)
|
thread._reset_internal_locks(True)
|
||||||
ident = get_ident()
|
ident = get_ident()
|
||||||
|
if isinstance(thread, _DummyThread):
|
||||||
|
thread.__class__ = _MainThread
|
||||||
|
thread._name = 'MainThread'
|
||||||
|
thread._daemonic = False
|
||||||
|
thread._set_tstate_lock()
|
||||||
thread._ident = ident
|
thread._ident = ident
|
||||||
new_active[ident] = thread
|
new_active[ident] = thread
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
When :func:`os.fork` is called from a foreign thread (aka ``_DummyThread``),
|
||||||
|
the type of the thread in a child process is changed to ``_MainThread``.
|
||||||
|
Also changed its name and daemonic status, it can be now joined.
|
Loading…
Add table
Add a link
Reference in a new issue