pFad - Phone/Frame/Anonymizer/Declutterfier! Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

URL: http://github.com/python/cpython/commit/fe119a08177feea27611450b0bb3901e330a2d58

gh-87135: threading.Lock: Raise rather than hang on Python finalizati… · python/cpython@fe119a0 · GitHub
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

6 files changed

Lines changed: 97 additions & 5 deletions

File tree

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)
pFad - Phonifier reborn

Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.





Check this box to remove all script contents from the fetched content.



Check this box to remove all images from the fetched content.


Check this box to remove all CSS styles from the fetched content.


Check this box to keep images inefficiently compressed and original size.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy