mirror of
https://github.com/python/cpython.git
synced 2025-08-12 04:49:01 +00:00
Issue #13992: The trashcan mechanism is now thread-safe. This eliminates
sporadic crashes in multi-thread programs when several long deallocator chains ran concurrently and involved subclasses of built-in container types. Note that the trashcan functions are part of the stable ABI, therefore they have to be kept around for binary compatibility of extensions.
This commit is contained in:
commit
5b4faae307
7 changed files with 140 additions and 9 deletions
|
@ -961,24 +961,33 @@ chain of N deallocations is broken into N / PyTrash_UNWIND_LEVEL pieces,
|
||||||
with the call stack never exceeding a depth of PyTrash_UNWIND_LEVEL.
|
with the call stack never exceeding a depth of PyTrash_UNWIND_LEVEL.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/* This is the old private API, invoked by the macros before 3.2.4.
|
||||||
|
Kept for binary compatibility of extensions using the stable ABI. */
|
||||||
PyAPI_FUNC(void) _PyTrash_deposit_object(PyObject*);
|
PyAPI_FUNC(void) _PyTrash_deposit_object(PyObject*);
|
||||||
PyAPI_FUNC(void) _PyTrash_destroy_chain(void);
|
PyAPI_FUNC(void) _PyTrash_destroy_chain(void);
|
||||||
PyAPI_DATA(int) _PyTrash_delete_nesting;
|
PyAPI_DATA(int) _PyTrash_delete_nesting;
|
||||||
PyAPI_DATA(PyObject *) _PyTrash_delete_later;
|
PyAPI_DATA(PyObject *) _PyTrash_delete_later;
|
||||||
|
|
||||||
|
/* The new thread-safe private API, invoked by the macros below. */
|
||||||
|
PyAPI_FUNC(void) _PyTrash_thread_deposit_object(PyObject*);
|
||||||
|
PyAPI_FUNC(void) _PyTrash_thread_destroy_chain(void);
|
||||||
|
|
||||||
#define PyTrash_UNWIND_LEVEL 50
|
#define PyTrash_UNWIND_LEVEL 50
|
||||||
|
|
||||||
#define Py_TRASHCAN_SAFE_BEGIN(op) \
|
#define Py_TRASHCAN_SAFE_BEGIN(op) \
|
||||||
if (_PyTrash_delete_nesting < PyTrash_UNWIND_LEVEL) { \
|
do { \
|
||||||
++_PyTrash_delete_nesting;
|
PyThreadState *_tstate = PyThreadState_GET(); \
|
||||||
|
if (_tstate->trash_delete_nesting < PyTrash_UNWIND_LEVEL) { \
|
||||||
|
++_tstate->trash_delete_nesting;
|
||||||
/* The body of the deallocator is here. */
|
/* The body of the deallocator is here. */
|
||||||
#define Py_TRASHCAN_SAFE_END(op) \
|
#define Py_TRASHCAN_SAFE_END(op) \
|
||||||
--_PyTrash_delete_nesting; \
|
--_tstate->trash_delete_nesting; \
|
||||||
if (_PyTrash_delete_later && _PyTrash_delete_nesting <= 0) \
|
if (_tstate->trash_delete_later && _tstate->trash_delete_nesting <= 0) \
|
||||||
_PyTrash_destroy_chain(); \
|
_PyTrash_thread_destroy_chain(); \
|
||||||
} \
|
} \
|
||||||
else \
|
else \
|
||||||
_PyTrash_deposit_object((PyObject*)op);
|
_PyTrash_thread_deposit_object((PyObject*)op); \
|
||||||
|
} while (0);
|
||||||
|
|
||||||
#ifndef Py_LIMITED_API
|
#ifndef Py_LIMITED_API
|
||||||
PyAPI_FUNC(void)
|
PyAPI_FUNC(void)
|
||||||
|
|
|
@ -114,6 +114,9 @@ typedef struct _ts {
|
||||||
PyObject *async_exc; /* Asynchronous exception to raise */
|
PyObject *async_exc; /* Asynchronous exception to raise */
|
||||||
long thread_id; /* Thread id where this tstate was created */
|
long thread_id; /* Thread id where this tstate was created */
|
||||||
|
|
||||||
|
int trash_delete_nesting;
|
||||||
|
PyObject *trash_delete_later;
|
||||||
|
|
||||||
/* XXX signal handlers should also be here */
|
/* XXX signal handlers should also be here */
|
||||||
|
|
||||||
} PyThreadState;
|
} PyThreadState;
|
||||||
|
|
|
@ -2,9 +2,15 @@ import unittest
|
||||||
from test.support import (verbose, refcount_test, run_unittest,
|
from test.support import (verbose, refcount_test, run_unittest,
|
||||||
strip_python_stderr)
|
strip_python_stderr)
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
import gc
|
import gc
|
||||||
import weakref
|
import weakref
|
||||||
|
|
||||||
|
try:
|
||||||
|
import threading
|
||||||
|
except ImportError:
|
||||||
|
threading = None
|
||||||
|
|
||||||
### Support code
|
### Support code
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -328,6 +334,69 @@ class GCTests(unittest.TestCase):
|
||||||
v = {1: v, 2: Ouch()}
|
v = {1: v, 2: Ouch()}
|
||||||
gc.disable()
|
gc.disable()
|
||||||
|
|
||||||
|
@unittest.skipUnless(threading, "test meaningless on builds without threads")
|
||||||
|
def test_trashcan_threads(self):
|
||||||
|
# Issue #13992: trashcan mechanism should be thread-safe
|
||||||
|
NESTING = 60
|
||||||
|
N_THREADS = 2
|
||||||
|
|
||||||
|
def sleeper_gen():
|
||||||
|
"""A generator that releases the GIL when closed or dealloc'ed."""
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
time.sleep(0.000001)
|
||||||
|
|
||||||
|
class C(list):
|
||||||
|
# Appending to a list is atomic, which avoids the use of a lock.
|
||||||
|
inits = []
|
||||||
|
dels = []
|
||||||
|
def __init__(self, alist):
|
||||||
|
self[:] = alist
|
||||||
|
C.inits.append(None)
|
||||||
|
def __del__(self):
|
||||||
|
# This __del__ is called by subtype_dealloc().
|
||||||
|
C.dels.append(None)
|
||||||
|
# `g` will release the GIL when garbage-collected. This
|
||||||
|
# helps assert subtype_dealloc's behaviour when threads
|
||||||
|
# switch in the middle of it.
|
||||||
|
g = sleeper_gen()
|
||||||
|
next(g)
|
||||||
|
# Now that __del__ is finished, subtype_dealloc will proceed
|
||||||
|
# to call list_dealloc, which also uses the trashcan mechanism.
|
||||||
|
|
||||||
|
def make_nested():
|
||||||
|
"""Create a sufficiently nested container object so that the
|
||||||
|
trashcan mechanism is invoked when deallocating it."""
|
||||||
|
x = C([])
|
||||||
|
for i in range(NESTING):
|
||||||
|
x = [C([x])]
|
||||||
|
del x
|
||||||
|
|
||||||
|
def run_thread():
|
||||||
|
"""Exercise make_nested() in a loop."""
|
||||||
|
while not exit:
|
||||||
|
make_nested()
|
||||||
|
|
||||||
|
old_switchinterval = sys.getswitchinterval()
|
||||||
|
sys.setswitchinterval(1e-5)
|
||||||
|
try:
|
||||||
|
exit = False
|
||||||
|
threads = []
|
||||||
|
for i in range(N_THREADS):
|
||||||
|
t = threading.Thread(target=run_thread)
|
||||||
|
threads.append(t)
|
||||||
|
for t in threads:
|
||||||
|
t.start()
|
||||||
|
time.sleep(1.0)
|
||||||
|
exit = True
|
||||||
|
for t in threads:
|
||||||
|
t.join()
|
||||||
|
finally:
|
||||||
|
sys.setswitchinterval(old_switchinterval)
|
||||||
|
gc.collect()
|
||||||
|
self.assertEqual(len(C.inits), len(C.dels))
|
||||||
|
|
||||||
def test_boom(self):
|
def test_boom(self):
|
||||||
class Boom:
|
class Boom:
|
||||||
def __getattr__(self, someattribute):
|
def __getattr__(self, someattribute):
|
||||||
|
|
|
@ -10,6 +10,11 @@ What's New in Python 3.3.1
|
||||||
Core and Builtins
|
Core and Builtins
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
|
- Issue #13992: The trashcan mechanism is now thread-safe. This eliminates
|
||||||
|
sporadic crashes in multi-thread programs when several long deallocator
|
||||||
|
chains ran concurrently and involved subclasses of built-in container
|
||||||
|
types.
|
||||||
|
|
||||||
- Issue #15839: Convert SystemErrors in super() to RuntimeErrors.
|
- Issue #15839: Convert SystemErrors in super() to RuntimeErrors.
|
||||||
|
|
||||||
- Issue #15846: Fix SystemError which happened when using ast.parse in an
|
- Issue #15846: Fix SystemError which happened when using ast.parse in an
|
||||||
|
|
|
@ -1954,6 +1954,18 @@ _PyTrash_deposit_object(PyObject *op)
|
||||||
_PyTrash_delete_later = op;
|
_PyTrash_delete_later = op;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The equivalent API, using per-thread state recursion info */
|
||||||
|
void
|
||||||
|
_PyTrash_thread_deposit_object(PyObject *op)
|
||||||
|
{
|
||||||
|
PyThreadState *tstate = PyThreadState_GET();
|
||||||
|
assert(PyObject_IS_GC(op));
|
||||||
|
assert(_Py_AS_GC(op)->gc.gc_refs == _PyGC_REFS_UNTRACKED);
|
||||||
|
assert(op->ob_refcnt == 0);
|
||||||
|
_Py_AS_GC(op)->gc.gc_prev = (PyGC_Head *) tstate->trash_delete_later;
|
||||||
|
tstate->trash_delete_later = op;
|
||||||
|
}
|
||||||
|
|
||||||
/* Dealloccate all the objects in the _PyTrash_delete_later list. Called when
|
/* Dealloccate all the objects in the _PyTrash_delete_later list. Called when
|
||||||
* the call-stack unwinds again.
|
* the call-stack unwinds again.
|
||||||
*/
|
*/
|
||||||
|
@ -1980,6 +1992,31 @@ _PyTrash_destroy_chain(void)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The equivalent API, using per-thread state recursion info */
|
||||||
|
void
|
||||||
|
_PyTrash_thread_destroy_chain(void)
|
||||||
|
{
|
||||||
|
PyThreadState *tstate = PyThreadState_GET();
|
||||||
|
while (tstate->trash_delete_later) {
|
||||||
|
PyObject *op = tstate->trash_delete_later;
|
||||||
|
destructor dealloc = Py_TYPE(op)->tp_dealloc;
|
||||||
|
|
||||||
|
tstate->trash_delete_later =
|
||||||
|
(PyObject*) _Py_AS_GC(op)->gc.gc_prev;
|
||||||
|
|
||||||
|
/* Call the deallocator directly. This used to try to
|
||||||
|
* fool Py_DECREF into calling it indirectly, but
|
||||||
|
* Py_DECREF was already called on this object, and in
|
||||||
|
* assorted non-release builds calling Py_DECREF again ends
|
||||||
|
* up distorting allocation statistics.
|
||||||
|
*/
|
||||||
|
assert(op->ob_refcnt == 0);
|
||||||
|
++tstate->trash_delete_nesting;
|
||||||
|
(*dealloc)(op);
|
||||||
|
--tstate->trash_delete_nesting;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#ifndef Py_TRACE_REFS
|
#ifndef Py_TRACE_REFS
|
||||||
/* For Py_LIMITED_API, we need an out-of-line version of _Py_Dealloc.
|
/* For Py_LIMITED_API, we need an out-of-line version of _Py_Dealloc.
|
||||||
Define this here, so we can undefine the macro. */
|
Define this here, so we can undefine the macro. */
|
||||||
|
|
|
@ -891,6 +891,7 @@ subtype_dealloc(PyObject *self)
|
||||||
{
|
{
|
||||||
PyTypeObject *type, *base;
|
PyTypeObject *type, *base;
|
||||||
destructor basedealloc;
|
destructor basedealloc;
|
||||||
|
PyThreadState *tstate = PyThreadState_GET();
|
||||||
|
|
||||||
/* Extract the type; we expect it to be a heap type */
|
/* Extract the type; we expect it to be a heap type */
|
||||||
type = Py_TYPE(self);
|
type = Py_TYPE(self);
|
||||||
|
@ -940,8 +941,10 @@ subtype_dealloc(PyObject *self)
|
||||||
/* See explanation at end of function for full disclosure */
|
/* See explanation at end of function for full disclosure */
|
||||||
PyObject_GC_UnTrack(self);
|
PyObject_GC_UnTrack(self);
|
||||||
++_PyTrash_delete_nesting;
|
++_PyTrash_delete_nesting;
|
||||||
|
++ tstate->trash_delete_nesting;
|
||||||
Py_TRASHCAN_SAFE_BEGIN(self);
|
Py_TRASHCAN_SAFE_BEGIN(self);
|
||||||
--_PyTrash_delete_nesting;
|
--_PyTrash_delete_nesting;
|
||||||
|
-- tstate->trash_delete_nesting;
|
||||||
/* DO NOT restore GC tracking at this point. weakref callbacks
|
/* DO NOT restore GC tracking at this point. weakref callbacks
|
||||||
* (if any, and whether directly here or indirectly in something we
|
* (if any, and whether directly here or indirectly in something we
|
||||||
* call) may trigger GC, and if self is tracked at that point, it
|
* call) may trigger GC, and if self is tracked at that point, it
|
||||||
|
@ -1020,8 +1023,10 @@ subtype_dealloc(PyObject *self)
|
||||||
|
|
||||||
endlabel:
|
endlabel:
|
||||||
++_PyTrash_delete_nesting;
|
++_PyTrash_delete_nesting;
|
||||||
|
++ tstate->trash_delete_nesting;
|
||||||
Py_TRASHCAN_SAFE_END(self);
|
Py_TRASHCAN_SAFE_END(self);
|
||||||
--_PyTrash_delete_nesting;
|
--_PyTrash_delete_nesting;
|
||||||
|
-- tstate->trash_delete_nesting;
|
||||||
|
|
||||||
/* Explanation of the weirdness around the trashcan macros:
|
/* Explanation of the weirdness around the trashcan macros:
|
||||||
|
|
||||||
|
|
|
@ -206,6 +206,9 @@ new_threadstate(PyInterpreterState *interp, int init)
|
||||||
tstate->c_profileobj = NULL;
|
tstate->c_profileobj = NULL;
|
||||||
tstate->c_traceobj = NULL;
|
tstate->c_traceobj = NULL;
|
||||||
|
|
||||||
|
tstate->trash_delete_nesting = 0;
|
||||||
|
tstate->trash_delete_later = NULL;
|
||||||
|
|
||||||
if (init)
|
if (init)
|
||||||
_PyThreadState_Init(tstate);
|
_PyThreadState_Init(tstate);
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue