Skip to content

Commit 1bd2c93

Browse files
miss-islingtonserhiy-storchakamarmarek
authored
[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 49785b0) Co-authored-by: Serhiy Storchaka <storchaka@gmail.com> Co-authored-by: Marek Marczykowski-Górecki <marmarek@invisiblethingslab.com>
1 parent d1f731f commit 1bd2c93

File tree

3 files changed

+98
-9
lines changed

3 files changed

+98
-9
lines changed

Lib/test/test_threading.py

Lines changed: 90 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ def tearDown(self):
115115

116116

117117
class ThreadTests(BaseTestCase):
118+
maxDiff = 9999
118119

119120
@cpython_only
120121
def test_name(self):
@@ -627,19 +628,25 @@ def test_main_thread_after_fork(self):
627628
import os, threading
628629
from test import support
629630
631+
ident = threading.get_ident()
630632
pid = os.fork()
631633
if pid == 0:
634+
print("current ident", threading.get_ident() == ident)
632635
main = threading.main_thread()
633-
print(main.name)
634-
print(main.ident == threading.current_thread().ident)
635-
print(main.ident == threading.get_ident())
636+
print("main", main.name)
637+
print("main ident", main.ident == ident)
638+
print("current is main", threading.current_thread() is main)
636639
else:
637640
support.wait_process(pid, exitcode=0)
638641
"""
639642
_, out, err = assert_python_ok("-c", code)
640643
data = out.decode().replace('\r', '')
641644
self.assertEqual(err, b"")
642-
self.assertEqual(data, "MainThread\nTrue\nTrue\n")
645+
self.assertEqual(data,
646+
"current ident True\n"
647+
"main MainThread\n"
648+
"main ident True\n"
649+
"current is main True\n")
643650

644651
@skip_unless_reliable_fork
645652
@unittest.skipUnless(hasattr(os, 'waitpid'), "test needs os.waitpid()")
@@ -649,15 +656,17 @@ def test_main_thread_after_fork_from_nonmain_thread(self):
649656
from test import support
650657
651658
def func():
659+
ident = threading.get_ident()
652660
with warnings.catch_warnings(record=True) as ws:
653661
warnings.filterwarnings(
654662
"always", category=DeprecationWarning)
655663
pid = os.fork()
656664
if pid == 0:
665+
print("current ident", threading.get_ident() == ident)
657666
main = threading.main_thread()
658-
print(main.name)
659-
print(main.ident == threading.current_thread().ident)
660-
print(main.ident == threading.get_ident())
667+
print("main", main.name, type(main).__name__)
668+
print("main ident", main.ident == ident)
669+
print("current is main", threading.current_thread() is main)
661670
# stdout is fully buffered because not a tty,
662671
# we have to flush before exit.
663672
sys.stdout.flush()
@@ -673,7 +682,80 @@ def func():
673682
_, out, err = assert_python_ok("-c", code)
674683
data = out.decode().replace('\r', '')
675684
self.assertEqual(err.decode('utf-8'), "")
676-
self.assertEqual(data, "Thread-1 (func)\nTrue\nTrue\n")
685+
self.assertEqual(data,
686+
"current ident True\n"
687+
"main Thread-1 (func) Thread\n"
688+
"main ident True\n"
689+
"current is main True\n"
690+
)
691+
692+
@unittest.skipIf(sys.platform in platforms_to_skip, "due to known OS bug")
693+
@support.requires_fork()
694+
@unittest.skipUnless(hasattr(os, 'waitpid'), "test needs os.waitpid()")
695+
def test_main_thread_after_fork_from_foreign_thread(self, create_dummy=False):
696+
code = """if 1:
697+
import os, threading, sys, traceback, _thread
698+
from test import support
699+
700+
def func(lock):
701+
ident = threading.get_ident()
702+
if %s:
703+
# call current_thread() before fork to allocate DummyThread
704+
current = threading.current_thread()
705+
print("current", current.name, type(current).__name__)
706+
print("ident in _active", ident in threading._active)
707+
# flush before fork, so child won't flush it again
708+
sys.stdout.flush()
709+
pid = os.fork()
710+
if pid == 0:
711+
print("current ident", threading.get_ident() == ident)
712+
main = threading.main_thread()
713+
print("main", main.name, type(main).__name__)
714+
print("main ident", main.ident == ident)
715+
print("current is main", threading.current_thread() is main)
716+
print("_dangling", [t.name for t in list(threading._dangling)])
717+
# stdout is fully buffered because not a tty,
718+
# we have to flush before exit.
719+
sys.stdout.flush()
720+
try:
721+
threading._shutdown()
722+
os._exit(0)
723+
except:
724+
traceback.print_exc()
725+
sys.stderr.flush()
726+
os._exit(1)
727+
else:
728+
try:
729+
support.wait_process(pid, exitcode=0)
730+
except Exception:
731+
# avoid 'could not acquire lock for
732+
# <_io.BufferedWriter name='<stderr>'> at interpreter shutdown,'
733+
traceback.print_exc()
734+
sys.stderr.flush()
735+
finally:
736+
lock.release()
737+
738+
join_lock = _thread.allocate_lock()
739+
join_lock.acquire()
740+
th = _thread.start_new_thread(func, (join_lock,))
741+
join_lock.acquire()
742+
""" % create_dummy
743+
# "DeprecationWarning: This process is multi-threaded, use of fork()
744+
# may lead to deadlocks in the child"
745+
_, out, err = assert_python_ok("-W", "ignore::DeprecationWarning", "-c", code)
746+
data = out.decode().replace('\r', '')
747+
self.assertEqual(err.decode(), "")
748+
self.assertEqual(data,
749+
("current Dummy-1 _DummyThread\n" if create_dummy else "") +
750+
f"ident in _active {create_dummy!s}\n" +
751+
"current ident True\n"
752+
"main MainThread _MainThread\n"
753+
"main ident True\n"
754+
"current is main True\n"
755+
"_dangling ['MainThread']\n")
756+
757+
def test_main_thread_after_fork_from_dummy_thread(self, create_dummy=False):
758+
self.test_main_thread_after_fork_from_foreign_thread(create_dummy=True)
677759

678760
def test_main_thread_during_shutdown(self):
679761
# bpo-31516: current_thread() should still point to the main thread

Lib/threading.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1460,7 +1460,6 @@ class _DummyThread(Thread):
14601460
def __init__(self):
14611461
Thread.__init__(self, name=_newname("Dummy-%d"),
14621462
daemon=_daemon_threads_allowed())
1463-
14641463
self._started.set()
14651464
self._set_ident()
14661465
if _HAVE_THREAD_NATIVE_ID:
@@ -1685,6 +1684,11 @@ def _after_fork():
16851684
# its new value since it can have changed.
16861685
thread._reset_internal_locks(True)
16871686
ident = get_ident()
1687+
if isinstance(thread, _DummyThread):
1688+
thread.__class__ = _MainThread
1689+
thread._name = 'MainThread'
1690+
thread._daemonic = False
1691+
thread._set_tstate_lock()
16881692
thread._ident = ident
16891693
new_active[ident] = thread
16901694
else:
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
When :func:`os.fork` is called from a foreign thread (aka ``_DummyThread``),
2+
the type of the thread in a child process is changed to ``_MainThread``.
3+
Also changed its name and daemonic status, it can be now joined.

0 commit comments

Comments
 (0)