Skip to content

Commit 0aefa4c

Browse files
[3.11] gh-102512: Turn _DummyThread into _MainThread after os.fork() called from a foreign thread (GH-113261)
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: Marek Marczykowski-Górecki <[email protected]>
1 parent 4d7c2c2 commit 0aefa4c

File tree

3 files changed

+99
-11
lines changed

3 files changed

+99
-11
lines changed

Lib/test/test_threading.py

Lines changed: 91 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ def tearDown(self):
106106

107107

108108
class ThreadTests(BaseTestCase):
109+
maxDiff = 9999
109110

110111
@cpython_only
111112
def test_name(self):
@@ -639,19 +640,25 @@ def test_main_thread_after_fork(self):
639640
import os, threading
640641
from test import support
641642
643+
ident = threading.get_ident()
642644
pid = os.fork()
643645
if pid == 0:
646+
print("current ident", threading.get_ident() == ident)
644647
main = threading.main_thread()
645-
print(main.name)
646-
print(main.ident == threading.current_thread().ident)
647-
print(main.ident == threading.get_ident())
648+
print("main", main.name)
649+
print("main ident", main.ident == ident)
650+
print("current is main", threading.current_thread() is main)
648651
else:
649652
support.wait_process(pid, exitcode=0)
650653
"""
651654
_, out, err = assert_python_ok("-c", code)
652655
data = out.decode().replace('\r', '')
653656
self.assertEqual(err, b"")
654-
self.assertEqual(data, "MainThread\nTrue\nTrue\n")
657+
self.assertEqual(data,
658+
"current ident True\n"
659+
"main MainThread\n"
660+
"main ident True\n"
661+
"current is main True\n")
655662

656663
@skip_unless_reliable_fork
657664
@unittest.skipUnless(hasattr(os, 'waitpid'), "test needs os.waitpid()")
@@ -661,26 +668,99 @@ def test_main_thread_after_fork_from_nonmain_thread(self):
661668
from test import support
662669
663670
def func():
671+
ident = threading.get_ident()
664672
pid = os.fork()
665673
if pid == 0:
674+
print("current ident", threading.get_ident() == ident)
666675
main = threading.main_thread()
667-
print(main.name)
668-
print(main.ident == threading.current_thread().ident)
669-
print(main.ident == threading.get_ident())
676+
print("main", main.name, type(main).__name__)
677+
print("main ident", main.ident == ident)
678+
print("current is main", threading.current_thread() is main)
670679
# stdout is fully buffered because not a tty,
671680
# we have to flush before exit.
672681
sys.stdout.flush()
673-
else:
674-
support.wait_process(pid, exitcode=0)
675682
676683
th = threading.Thread(target=func)
677684
th.start()
678685
th.join()
679686
"""
680687
_, out, err = assert_python_ok("-c", code)
681688
data = out.decode().replace('\r', '')
682-
self.assertEqual(err, b"")
683-
self.assertEqual(data, "Thread-1 (func)\nTrue\nTrue\n")
689+
self.assertEqual(err.decode('utf-8'), "")
690+
self.assertEqual(data,
691+
"current ident True\n"
692+
"main Thread-1 (func) Thread\n"
693+
"main ident True\n"
694+
"current is main True\n"
695+
)
696+
697+
@unittest.skipIf(sys.platform in platforms_to_skip, "due to known OS bug")
698+
@support.requires_fork()
699+
@unittest.skipUnless(hasattr(os, 'waitpid'), "test needs os.waitpid()")
700+
def test_main_thread_after_fork_from_foreign_thread(self, create_dummy=False):
701+
code = """if 1:
702+
import os, threading, sys, traceback, _thread
703+
from test import support
704+
705+
def func(lock):
706+
ident = threading.get_ident()
707+
if %s:
708+
# call current_thread() before fork to allocate DummyThread
709+
current = threading.current_thread()
710+
print("current", current.name, type(current).__name__)
711+
print("ident in _active", ident in threading._active)
712+
# flush before fork, so child won't flush it again
713+
sys.stdout.flush()
714+
pid = os.fork()
715+
if pid == 0:
716+
print("current ident", threading.get_ident() == ident)
717+
main = threading.main_thread()
718+
print("main", main.name, type(main).__name__)
719+
print("main ident", main.ident == ident)
720+
print("current is main", threading.current_thread() is main)
721+
print("_dangling", [t.name for t in list(threading._dangling)])
722+
# stdout is fully buffered because not a tty,
723+
# we have to flush before exit.
724+
sys.stdout.flush()
725+
try:
726+
threading._shutdown()
727+
os._exit(0)
728+
except:
729+
traceback.print_exc()
730+
sys.stderr.flush()
731+
os._exit(1)
732+
else:
733+
try:
734+
support.wait_process(pid, exitcode=0)
735+
except Exception:
736+
# avoid 'could not acquire lock for
737+
# <_io.BufferedWriter name='<stderr>'> at interpreter shutdown,'
738+
traceback.print_exc()
739+
sys.stderr.flush()
740+
finally:
741+
lock.release()
742+
743+
join_lock = _thread.allocate_lock()
744+
join_lock.acquire()
745+
th = _thread.start_new_thread(func, (join_lock,))
746+
join_lock.acquire()
747+
""" % create_dummy
748+
# "DeprecationWarning: This process is multi-threaded, use of fork()
749+
# may lead to deadlocks in the child"
750+
_, out, err = assert_python_ok("-W", "ignore::DeprecationWarning", "-c", code)
751+
data = out.decode().replace('\r', '')
752+
self.assertEqual(err.decode(), "")
753+
self.assertEqual(data,
754+
("current Dummy-1 _DummyThread\n" if create_dummy else "") +
755+
f"ident in _active {create_dummy!s}\n" +
756+
"current ident True\n"
757+
"main MainThread _MainThread\n"
758+
"main ident True\n"
759+
"current is main True\n"
760+
"_dangling ['MainThread']\n")
761+
762+
def test_main_thread_after_fork_from_dummy_thread(self, create_dummy=False):
763+
self.test_main_thread_after_fork_from_foreign_thread(create_dummy=True)
684764

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

Lib/threading.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1651,6 +1651,11 @@ def _after_fork():
16511651
# its new value since it can have changed.
16521652
thread._reset_internal_locks(True)
16531653
ident = get_ident()
1654+
if isinstance(thread, _DummyThread):
1655+
thread.__class__ = _MainThread
1656+
thread._name = 'MainThread'
1657+
thread._daemonic = False
1658+
thread._set_tstate_lock()
16541659
thread._ident = ident
16551660
new_active[ident] = thread
16561661
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)