Skip to content

Commit 49785b0

Browse files
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. Co-authored-by: Marek Marczykowski-Górecki <[email protected]>
1 parent 9f7176d commit 49785b0

File tree

3 files changed

+101
-9
lines changed

3 files changed

+101
-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):
@@ -676,19 +677,25 @@ def test_main_thread_after_fork(self):
676677
import os, threading
677678
from test import support
678679
680+
ident = threading.get_ident()
679681
pid = os.fork()
680682
if pid == 0:
683+
print("current ident", threading.get_ident() == ident)
681684
main = threading.main_thread()
682-
print(main.name)
683-
print(main.ident == threading.current_thread().ident)
684-
print(main.ident == threading.get_ident())
685+
print("main", main.name)
686+
print("main ident", main.ident == ident)
687+
print("current is main", threading.current_thread() is main)
685688
else:
686689
support.wait_process(pid, exitcode=0)
687690
"""
688691
_, out, err = assert_python_ok("-c", code)
689692
data = out.decode().replace('\r', '')
690693
self.assertEqual(err, b"")
691-
self.assertEqual(data, "MainThread\nTrue\nTrue\n")
694+
self.assertEqual(data,
695+
"current ident True\n"
696+
"main MainThread\n"
697+
"main ident True\n"
698+
"current is main True\n")
692699

693700
@skip_unless_reliable_fork
694701
@unittest.skipUnless(hasattr(os, 'waitpid'), "test needs os.waitpid()")
@@ -698,15 +705,17 @@ def test_main_thread_after_fork_from_nonmain_thread(self):
698705
from test import support
699706
700707
def func():
708+
ident = threading.get_ident()
701709
with warnings.catch_warnings(record=True) as ws:
702710
warnings.filterwarnings(
703711
"always", category=DeprecationWarning)
704712
pid = os.fork()
705713
if pid == 0:
714+
print("current ident", threading.get_ident() == ident)
706715
main = threading.main_thread()
707-
print(main.name)
708-
print(main.ident == threading.current_thread().ident)
709-
print(main.ident == threading.get_ident())
716+
print("main", main.name, type(main).__name__)
717+
print("main ident", main.ident == ident)
718+
print("current is main", threading.current_thread() is main)
710719
# stdout is fully buffered because not a tty,
711720
# we have to flush before exit.
712721
sys.stdout.flush()
@@ -722,7 +731,80 @@ def func():
722731
_, out, err = assert_python_ok("-c", code)
723732
data = out.decode().replace('\r', '')
724733
self.assertEqual(err.decode('utf-8'), "")
725-
self.assertEqual(data, "Thread-1 (func)\nTrue\nTrue\n")
734+
self.assertEqual(data,
735+
"current ident True\n"
736+
"main Thread-1 (func) Thread\n"
737+
"main ident True\n"
738+
"current is main True\n"
739+
)
740+
741+
@unittest.skipIf(sys.platform in platforms_to_skip, "due to known OS bug")
742+
@support.requires_fork()
743+
@unittest.skipUnless(hasattr(os, 'waitpid'), "test needs os.waitpid()")
744+
def test_main_thread_after_fork_from_foreign_thread(self, create_dummy=False):
745+
code = """if 1:
746+
import os, threading, sys, traceback, _thread
747+
from test import support
748+
749+
def func(lock):
750+
ident = threading.get_ident()
751+
if %s:
752+
# call current_thread() before fork to allocate DummyThread
753+
current = threading.current_thread()
754+
print("current", current.name, type(current).__name__)
755+
print("ident in _active", ident in threading._active)
756+
# flush before fork, so child won't flush it again
757+
sys.stdout.flush()
758+
pid = os.fork()
759+
if pid == 0:
760+
print("current ident", threading.get_ident() == ident)
761+
main = threading.main_thread()
762+
print("main", main.name, type(main).__name__)
763+
print("main ident", main.ident == ident)
764+
print("current is main", threading.current_thread() is main)
765+
print("_dangling", [t.name for t in list(threading._dangling)])
766+
# stdout is fully buffered because not a tty,
767+
# we have to flush before exit.
768+
sys.stdout.flush()
769+
try:
770+
threading._shutdown()
771+
os._exit(0)
772+
except:
773+
traceback.print_exc()
774+
sys.stderr.flush()
775+
os._exit(1)
776+
else:
777+
try:
778+
support.wait_process(pid, exitcode=0)
779+
except Exception:
780+
# avoid 'could not acquire lock for
781+
# <_io.BufferedWriter name='<stderr>'> at interpreter shutdown,'
782+
traceback.print_exc()
783+
sys.stderr.flush()
784+
finally:
785+
lock.release()
786+
787+
join_lock = _thread.allocate_lock()
788+
join_lock.acquire()
789+
th = _thread.start_new_thread(func, (join_lock,))
790+
join_lock.acquire()
791+
""" % create_dummy
792+
# "DeprecationWarning: This process is multi-threaded, use of fork()
793+
# may lead to deadlocks in the child"
794+
_, out, err = assert_python_ok("-W", "ignore::DeprecationWarning", "-c", code)
795+
data = out.decode().replace('\r', '')
796+
self.assertEqual(err.decode(), "")
797+
self.assertEqual(data,
798+
("current Dummy-1 _DummyThread\n" if create_dummy else "") +
799+
f"ident in _active {create_dummy!s}\n" +
800+
"current ident True\n"
801+
"main MainThread _MainThread\n"
802+
"main ident True\n"
803+
"current is main True\n"
804+
"_dangling ['MainThread']\n")
805+
806+
def test_main_thread_after_fork_from_dummy_thread(self, create_dummy=False):
807+
self.test_main_thread_after_fork_from_foreign_thread(create_dummy=True)
726808

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

Lib/threading.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1489,7 +1489,6 @@ class _DummyThread(Thread):
14891489
def __init__(self):
14901490
Thread.__init__(self, name=_newname("Dummy-%d"),
14911491
daemon=_daemon_threads_allowed())
1492-
14931492
self._started.set()
14941493
self._set_ident()
14951494
if _HAVE_THREAD_NATIVE_ID:
@@ -1508,6 +1507,14 @@ def is_alive(self):
15081507
def join(self, timeout=None):
15091508
raise RuntimeError("cannot join a dummy thread")
15101509

1510+
def _after_fork(self, new_ident=None):
1511+
if new_ident is not None:
1512+
self.__class__ = _MainThread
1513+
self._name = 'MainThread'
1514+
self._daemonic = False
1515+
self._set_tstate_lock()
1516+
Thread._after_fork(self, new_ident=new_ident)
1517+
15111518

15121519
# Global API functions
15131520

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)