Skip to content

Commit

Permalink
[3.11] gh-102512: Turn _DummyThread into _MainThread after os.fork() …
Browse files Browse the repository at this point in the history
…called from a foreign thread (GH-113261) (GH-114431)

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 <marmarek@invisiblethingslab.com>
  • Loading branch information
serhiy-storchaka and marmarek committed Jan 22, 2024
1 parent 459726f commit a0f30b0
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 11 deletions.
102 changes: 91 additions & 11 deletions Lib/test/test_threading.py
Expand Up @@ -106,6 +106,7 @@ def tearDown(self):


class ThreadTests(BaseTestCase):
maxDiff = 9999

@cpython_only
def test_name(self):
Expand Down Expand Up @@ -639,19 +640,25 @@ def test_main_thread_after_fork(self):
import os, threading
from test import support
ident = threading.get_ident()
pid = os.fork()
if pid == 0:
print("current ident", threading.get_ident() == ident)
main = threading.main_thread()
print(main.name)
print(main.ident == threading.current_thread().ident)
print(main.ident == threading.get_ident())
print("main", main.name)
print("main ident", main.ident == ident)
print("current is main", threading.current_thread() is main)
else:
support.wait_process(pid, exitcode=0)
"""
_, out, err = assert_python_ok("-c", code)
data = out.decode().replace('\r', '')
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
@unittest.skipUnless(hasattr(os, 'waitpid'), "test needs os.waitpid()")
Expand All @@ -661,26 +668,99 @@ def test_main_thread_after_fork_from_nonmain_thread(self):
from test import support
def func():
ident = threading.get_ident()
pid = os.fork()
if pid == 0:
print("current ident", threading.get_ident() == ident)
main = threading.main_thread()
print(main.name)
print(main.ident == threading.current_thread().ident)
print(main.ident == threading.get_ident())
print("main", main.name, type(main).__name__)
print("main ident", main.ident == ident)
print("current is main", threading.current_thread() is main)
# stdout is fully buffered because not a tty,
# we have to flush before exit.
sys.stdout.flush()
else:
support.wait_process(pid, exitcode=0)
th = threading.Thread(target=func)
th.start()
th.join()
"""
_, out, err = assert_python_ok("-c", code)
data = out.decode().replace('\r', '')
self.assertEqual(err, b"")
self.assertEqual(data, "Thread-1 (func)\nTrue\nTrue\n")
self.assertEqual(err.decode('utf-8'), "")
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):
# bpo-31516: current_thread() should still point to the main thread
Expand Down
5 changes: 5 additions & 0 deletions Lib/threading.py
Expand Up @@ -1651,6 +1651,11 @@ def _after_fork():
# its new value since it can have changed.
thread._reset_internal_locks(True)
ident = get_ident()
if isinstance(thread, _DummyThread):
thread.__class__ = _MainThread
thread._name = 'MainThread'
thread._daemonic = False
thread._set_tstate_lock()
thread._ident = ident
new_active[ident] = thread
else:
Expand Down
@@ -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.

0 comments on commit a0f30b0

Please sign in to comment.