Skip to content

Commit 20a2b04

Browse files
committed
pythongh-106883 Fix deadlock in threaded application
When using threaded applications, there is a high risk of a deadlock in the intepreter. It's a lock ordering deadlock with HEAD_LOCK(&_PyRuntime); and the GIL. By disabling the GC during the _PyThread_CurrentFrames() and _PyThread_CurrentExceptions() calls fixes the issue.
1 parent 625b0f9 commit 20a2b04

File tree

2 files changed

+63
-13
lines changed

2 files changed

+63
-13
lines changed

Lib/test/test_sys.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,55 @@ def g456():
471471
leave_g.set()
472472
t.join()
473473

474+
@threading_helper.reap_threads
475+
@threading_helper.requires_working_threading()
476+
@support.requires_fork()
477+
def test_current_frames_exceptions_deadlock(self):
478+
"""
479+
Try to reproduce the bug raised in GH-106883 and GH-116969.
480+
"""
481+
import threading
482+
import time
483+
484+
class MockObject:
485+
def __init__(self):
486+
# Create some garbage
487+
self._list = list(range(10000))
488+
# Call the functions under test
489+
self._trace = sys._current_frames()
490+
self._exceptions = sys._current_exceptions()
491+
492+
def thread_function(num_objects):
493+
obj = None
494+
for _ in range(num_objects):
495+
# The sleep is needed to have a syscall: in interrupts the
496+
# current thread, releases the GIL and gives way to other
497+
# threads to be executed. In this way there are more chances
498+
# to reproduce the bug.
499+
time.sleep(0)
500+
obj = MockObject()
501+
502+
NUM_OBJECTS = 25
503+
NUM_THREADS = 1000
504+
505+
# 60 seconds should be enough for the test to be executed: if it
506+
# is more than 60 seconds it means that the process is in deadlock
507+
# hence the test fails
508+
TIMEOUT = 60
509+
510+
# Test the sys._current_frames and sys._current_exceptions calls
511+
pid = os.fork()
512+
if pid: # parent process
513+
support.wait_process(pid, exitcode=0, timeout=TIMEOUT)
514+
else: # child process
515+
# Run the actual test in the forked process.
516+
for i in range(NUM_THREADS):
517+
thread = threading.Thread(
518+
target=thread_function, args=(NUM_OBJECTS,)
519+
)
520+
thread.start()
521+
os._exit(0)
522+
474523
@threading_helper.reap_threads
475524
@threading_helper.requires_working_threading()
476525
def test_current_exceptions(self):

Python/pystate.c

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
/* Thread and interpreter state structures and their interfaces */
33

44
#include "Python.h"
5-
#include "objimpl.h"
65
#include "pycore_ceval.h"
76
#include "pycore_code.h" // stats
87
#include "pycore_frame.h"
@@ -1389,10 +1388,6 @@ PyThreadState_Next(PyThreadState *tstate) {
13891388
PyObject *
13901389
_PyThread_CurrentFrames(void)
13911390
{
1392-
// Disable the GC as this can cause a deadlock the interpreter.
1393-
// See issues 116969 and 106883.
1394-
PyGC_Disable();
1395-
13961391
PyThreadState *tstate = _PyThreadState_GET();
13971392
if (_PySys_Audit(tstate, "sys._current_frames", NULL) < 0) {
13981393
return NULL;
@@ -1403,6 +1398,9 @@ _PyThread_CurrentFrames(void)
14031398
return NULL;
14041399
}
14051400

1401+
// gh-106883: Disable the GC as this can cause the interpreter to deadlock
1402+
int gc_was_enabled = PyGC_Disable();
1403+
14061404
/* for i in all interpreters:
14071405
* for t in all of i's thread states:
14081406
* if t's frame isn't NULL, map t's id to its frame
@@ -1446,19 +1444,17 @@ _PyThread_CurrentFrames(void)
14461444
done:
14471445
HEAD_UNLOCK(runtime);
14481446

1449-
// Once we release the runtime, the GC can be reenabled.
1450-
PyGC_Enable();
1447+
// Once the runtime is released, the GC can be reenabled.
1448+
if (gc_was_enabled) {
1449+
PyGC_Enable();
1450+
}
14511451

14521452
return result;
14531453
}
14541454

14551455
PyObject *
14561456
_PyThread_CurrentExceptions(void)
14571457
{
1458-
// Disable the GC as this can cause a deadlock the interpreter.
1459-
// See issues 116969 and 106883.
1460-
PyGC_Disable();
1461-
14621458
PyThreadState *tstate = _PyThreadState_GET();
14631459

14641460
_Py_EnsureTstateNotNULL(tstate);
@@ -1472,6 +1468,9 @@ _PyThread_CurrentExceptions(void)
14721468
return NULL;
14731469
}
14741470

1471+
// gh-106883: Disable the GC as this can cause the interpreter to deadlock
1472+
int gc_was_enabled = PyGC_Disable();
1473+
14751474
/* for i in all interpreters:
14761475
* for t in all of i's thread states:
14771476
* if t's frame isn't NULL, map t's id to its frame
@@ -1513,8 +1512,10 @@ _PyThread_CurrentExceptions(void)
15131512
done:
15141513
HEAD_UNLOCK(runtime);
15151514

1516-
// Once we release the runtime, the GC can be reenabled.
1517-
PyGC_Enable();
1515+
// Once the runtime is released, the GC can be reenabled.
1516+
if (gc_was_enabled) {
1517+
PyGC_Enable();
1518+
}
15181519

15191520
return result;
15201521
}

0 commit comments

Comments
 (0)