| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 845263a commit fe119a0
6 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -429,7 +429,9 @@ The following exceptions are the exceptions that are usually raised. | |||
| 429 | 429 | ||
| 430 | 430 | * Creating a new Python thread. | |
| 431 | 431 | * :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. | ||
| 433 | 435 | ||
| 434 | 436 | See also the :func:`sys.is_finalizing` function. | |
| 435 | 437 | ||
@@ -440,6 +442,11 @@ The following exceptions are the exceptions that are usually raised. | |||
| 440 | 442 | ||
| 441 | 443 | :meth:`threading.Thread.join` can now raise this exception. | |
| 442 | 444 | ||
| 445 | + .. versionchanged:: next | ||
| 446 | + | ||
| 447 | + This exception may be raised when acquiring :meth:`threading.Lock` | ||
| 448 | + or :meth:`threading.RLock`. | ||
| 449 | + | ||
| 443 | 450 | .. exception:: RecursionError | |
| 444 | 451 | ||
| 445 | 452 | This exception is derived from :exc:`RuntimeError`. It is raised when the | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -51,6 +51,11 @@ typedef enum _PyLockFlags { | |||
| 51 | 51 | ||
| 52 | 52 | // Fail if interrupted by a signal while waiting on the lock. | |
| 53 | 53 | _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, | ||
| 54 | 59 | } _PyLockFlags; | |
| 55 | 60 | ||
| 56 | 61 | // Lock a mutex with an optional timeout and additional options. See | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1247,6 +1247,61 @@ def __del__(self): | |||
| 1247 | 1247 | self.assertEqual(err, b"") | |
| 1248 | 1248 | self.assertIn(b"all clear", out) | |
| 1249 | 1249 | ||
| 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 | + | ||
| 1250 | 1305 | def test_start_new_thread_failed(self): | |
| 1251 | 1306 | # gh-109746: if Python fails to start newly created thread | |
| 1252 | 1307 | # due to failure of underlying PyThread_start_new_thread() call, | |
| Original file line number | Diff line number | Diff 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. | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -834,9 +834,14 @@ lock_PyThread_acquire_lock(PyObject *op, PyObject *args, PyObject *kwds) | |||
| 834 | 834 | return NULL; | |
| 835 | 835 | } | |
| 836 | 836 | ||
| 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); | ||
| 839 | 840 | if (r == PY_LOCK_INTR) { | |
| 841 | + assert(PyErr_Occurred()); | ||
| 842 | + return NULL; | ||
| 843 | + } | ||
| 844 | + if (r == PY_LOCK_FAILURE && PyErr_Occurred()) { | ||
| 840 | 845 | return NULL; | |
| 841 | 846 | } | |
| 842 | 847 | ||
@@ -1054,9 +1059,14 @@ rlock_acquire(PyObject *op, PyObject *args, PyObject *kwds) | |||
| 1054 | 1059 | return NULL; | |
| 1055 | 1060 | } | |
| 1056 | 1061 | ||
| 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); | ||
| 1059 | 1065 | if (r == PY_LOCK_INTR) { | |
| 1066 | + assert(PyErr_Occurred()); | ||
| 1067 | + return NULL; | ||
| 1068 | + } | ||
| 1069 | + if (r == PY_LOCK_FAILURE && PyErr_Occurred()) { | ||
| 1060 | 1070 | return NULL; | |
| 1061 | 1071 | } | |
| 1062 | 1072 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -95,6 +95,18 @@ _PyMutex_LockTimed(PyMutex *m, PyTime_t timeout, _PyLockFlags flags) | |||
| 95 | 95 | if (timeout == 0) { | |
| 96 | 96 | return PY_LOCK_FAILURE; | |
| 97 | 97 | } | |
| 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 | + } | ||
| 98 | 110 | ||
| 99 | 111 | uint8_t newv = v; | |
| 100 | 112 | if (!(v & _Py_HAS_PARKED)) { | |
| Back | FazBrowse Home | New Git URL |
0 commit comments