gh-87135: threading.Lock: Raise rather than hang on Python finalizati… · python/cpython@fe119a0 · GitHub | Latest TMZ Celebrity News & Gossip | Watch TMZ Live
Skip to content

Commit fe119a0

Browse files
authored
gh-87135: threading.Lock: Raise rather than hang on Python finalization (GH-135991)
After Python finalization gets to the point where no other thread can attach thread state, attempting to acquire a Python lock must hang. Raise PythonFinalizationError instead of hanging.
1 parent 845263a commit fe119a0

File tree

6 files changed

+97
-5
lines changed

6 files changed

+97
-5
lines changed

Doc/library/exceptions.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,9 @@ The following exceptions are the exceptions that are usually raised.
429429

430430
* Creating a new Python thread.
431431
* :meth:`Joining <threading.Thread.join>` a running daemon thread.
432-
* :func:`os.fork`.
432+
* :func:`os.fork`,
433+
* acquiring a lock such as :class:`threading.Lock`, when it is known that
434+
the operation would otherwise deadlock.
433435

434436
See also the :func:`sys.is_finalizing` function.
435437

@@ -440,6 +442,11 @@ The following exceptions are the exceptions that are usually raised.
440442

441443
:meth:`threading.Thread.join` can now raise this exception.
442444

445+
.. versionchanged:: next
446+
447+
This exception may be raised when acquiring :meth:`threading.Lock`
448+
or :meth:`threading.RLock`.
449+
443450
.. exception:: RecursionError
444451

445452
This exception is derived from :exc:`RuntimeError`. It is raised when the

Include/internal/pycore_lock.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ typedef enum _PyLockFlags {
5151

5252
// Fail if interrupted by a signal while waiting on the lock.
5353
_PY_FAIL_IF_INTERRUPTED = 4,
54+
55+
// Locking & unlocking this lock requires attached thread state.
56+
// If locking returns PY_LOCK_FAILURE, a Python exception *may* be raised.
57+
// (Intended for use with _PY_LOCK_HANDLE_SIGNALS and _PY_LOCK_DETACH.)
58+
_PY_LOCK_PYTHONLOCK = 8,
5459
} _PyLockFlags;
5560

5661
// Lock a mutex with an optional timeout and additional options. See

Lib/test/test_threading.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,6 +1247,61 @@ def __del__(self):
12471247
self.assertEqual(err, b"")
12481248
self.assertIn(b"all clear", out)
12491249

1250+
@support.subTests('lock_class_name', ['Lock', 'RLock'])
1251+
def test_acquire_daemon_thread_lock_in_finalization(self, lock_class_name):
1252+
# gh-123940: Py_Finalize() prevents other threads from running Python
1253+
# code (and so, releasing locks), so acquiring a locked lock can not
1254+
# succeed.
1255+
# We raise an exception rather than hang.
1256+
code = textwrap.dedent(f"""
1257+
import threading
1258+
import time
1259+
1260+
thread_started_event = threading.Event()
1261+
1262+
lock = threading.{lock_class_name}()
1263+
def loop():
1264+
if {lock_class_name!r} == 'RLock':
1265+
lock.acquire()
1266+
with lock:
1267+
thread_started_event.set()
1268+
while True:
1269+
time.sleep(1)
1270+
1271+
uncontested_lock = threading.{lock_class_name}()
1272+
1273+
class Cycle:
1274+
def __init__(self):
1275+
self.self_ref = self
1276+
self.thr = threading.Thread(
1277+
target=loop, daemon=True)
1278+
self.thr.start()
1279+
thread_started_event.wait()
1280+
1281+
def __del__(self):
1282+
assert self.thr.is_alive()
1283+
1284+
# We *can* acquire an unlocked lock
1285+
uncontested_lock.acquire()
1286+
if {lock_class_name!r} == 'RLock':
1287+
uncontested_lock.acquire()
1288+
1289+
# Acquiring a locked one fails
1290+
try:
1291+
lock.acquire()
1292+
except PythonFinalizationError:
1293+
assert self.thr.is_alive()
1294+
print('got the correct exception!')
1295+
1296+
# Cycle holds a reference to itself, which ensures it is
1297+
# cleaned up during the GC that runs after daemon threads
1298+
# have been forced to exit during finalization.
1299+
Cycle()
1300+
""")
1301+
rc, out, err = assert_python_ok("-c", code)
1302+
self.assertEqual(err, b"")
1303+
self.assertIn(b"got the correct exception", out)
1304+
12501305
def test_start_new_thread_failed(self):
12511306
# gh-109746: if Python fails to start newly created thread
12521307
# due to failure of underlying PyThread_start_new_thread() call,
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Acquiring a :class:`threading.Lock` or :class:`threading.RLock` at interpreter
2+
shutdown will raise :exc:`PythonFinalizationError` if Python can determine
3+
that it would otherwise deadlock.

Modules/_threadmodule.c

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -834,9 +834,14 @@ lock_PyThread_acquire_lock(PyObject *op, PyObject *args, PyObject *kwds)
834834
return NULL;
835835
}
836836

837-
PyLockStatus r = _PyMutex_LockTimed(&self->lock, timeout,
838-
_PY_LOCK_HANDLE_SIGNALS | _PY_LOCK_DETACH);
837+
PyLockStatus r = _PyMutex_LockTimed(
838+
&self->lock, timeout,
839+
_PY_LOCK_PYTHONLOCK | _PY_LOCK_HANDLE_SIGNALS | _PY_LOCK_DETACH);
839840
if (r == PY_LOCK_INTR) {
841+
assert(PyErr_Occurred());
842+
return NULL;
843+
}
844+
if (r == PY_LOCK_FAILURE && PyErr_Occurred()) {
840845
return NULL;
841846
}
842847

@@ -1054,9 +1059,14 @@ rlock_acquire(PyObject *op, PyObject *args, PyObject *kwds)
10541059
return NULL;
10551060
}
10561061

1057-
PyLockStatus r = _PyRecursiveMutex_LockTimed(&self->lock, timeout,
1058-
_PY_LOCK_HANDLE_SIGNALS | _PY_LOCK_DETACH);
1062+
PyLockStatus r = _PyRecursiveMutex_LockTimed(
1063+
&self->lock, timeout,
1064+
_PY_LOCK_PYTHONLOCK | _PY_LOCK_HANDLE_SIGNALS | _PY_LOCK_DETACH);
10591065
if (r == PY_LOCK_INTR) {
1066+
assert(PyErr_Occurred());
1067+
return NULL;
1068+
}
1069+
if (r == PY_LOCK_FAILURE && PyErr_Occurred()) {
10601070
return NULL;
10611071
}
10621072

Python/lock.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,18 @@ _PyMutex_LockTimed(PyMutex *m, PyTime_t timeout, _PyLockFlags flags)
9595
if (timeout == 0) {
9696
return PY_LOCK_FAILURE;
9797
}
98+
if ((flags & _PY_LOCK_PYTHONLOCK) && Py_IsFinalizing()) {
99+
// At this phase of runtime shutdown, only the finalization thread
100+
// can have attached thread state; others hang if they try
101+
// attaching. And since operations on this lock requires attached
102+
// thread state (_PY_LOCK_PYTHONLOCK), the finalization thread is
103+
// running this code, and no other thread can unlock.
104+
// Raise rather than hang. (_PY_LOCK_PYTHONLOCK allows raising
105+
// exceptons.)
106+
PyErr_SetString(PyExc_PythonFinalizationError,
107+
"cannot acquire lock at interpreter finalization");
108+
return PY_LOCK_FAILURE;
109+
}
98110

99111
uint8_t newv = v;
100112
if (!(v & _Py_HAS_PARKED)) {

0 commit comments

Comments
 (0)

TMZ Celebrity News – Breaking Stories, Videos & Gossip

Looking for the latest TMZ celebrity news? You've come to the right place. From shocking Hollywood scandals to exclusive videos, TMZ delivers it all in real time.

Whether it’s a red carpet slip-up, a viral paparazzi moment, or a legal drama involving your favorite stars, TMZ news is always first to break the story. Stay in the loop with daily updates, insider tips, and jaw-dropping photos.

🎥 Watch TMZ Live

TMZ Live brings you daily celebrity news and interviews straight from the TMZ newsroom. Don’t miss a beat—watch now and see what’s trending in Hollywood.