Skip to content

Commit 5b4faae

Browse files
committed
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.
2 parents 11946fb + 56cd62c commit 5b4faae

7 files changed

Lines changed: 140 additions & 9 deletions

File tree

Include/object.h

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -961,24 +961,33 @@ chain of N deallocations is broken into N / PyTrash_UNWIND_LEVEL pieces,
961961
with the call stack never exceeding a depth of PyTrash_UNWIND_LEVEL.
962962
*/
963963

964+
/* This is the old private API, invoked by the macros before 3.2.4.
965+
Kept for binary compatibility of extensions using the stable ABI. */
964966
PyAPI_FUNC(void) _PyTrash_deposit_object(PyObject*);
965967
PyAPI_FUNC(void) _PyTrash_destroy_chain(void);
966968
PyAPI_DATA(int) _PyTrash_delete_nesting;
967969
PyAPI_DATA(PyObject *) _PyTrash_delete_later;
968970

971+
/* The new thread-safe private API, invoked by the macros below. */
972+
PyAPI_FUNC(void) _PyTrash_thread_deposit_object(PyObject*);
973+
PyAPI_FUNC(void) _PyTrash_thread_destroy_chain(void);
974+
969975
#define PyTrash_UNWIND_LEVEL 50
970976

971977
#define Py_TRASHCAN_SAFE_BEGIN(op) \
972-
if (_PyTrash_delete_nesting < PyTrash_UNWIND_LEVEL) { \
973-
++_PyTrash_delete_nesting;
974-
/* The body of the deallocator is here. */
978+
do { \
979+
PyThreadState *_tstate = PyThreadState_GET(); \
980+
if (_tstate->trash_delete_nesting < PyTrash_UNWIND_LEVEL) { \
981+
++_tstate->trash_delete_nesting;
982+
/* The body of the deallocator is here. */
975983
#define Py_TRASHCAN_SAFE_END(op) \
976-
--_PyTrash_delete_nesting; \
977-
if (_PyTrash_delete_later && _PyTrash_delete_nesting <= 0) \
978-
_PyTrash_destroy_chain(); \
979-
} \
980-
else \
981-
_PyTrash_deposit_object((PyObject*)op);
984+
--_tstate->trash_delete_nesting; \
985+
if (_tstate->trash_delete_later && _tstate->trash_delete_nesting <= 0) \
986+
_PyTrash_thread_destroy_chain(); \
987+
} \
988+
else \
989+
_PyTrash_thread_deposit_object((PyObject*)op); \
990+
} while (0);
982991

983992
#ifndef Py_LIMITED_API
984993
PyAPI_FUNC(void)

Include/pystate.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ typedef struct _ts {
114114
PyObject *async_exc; /* Asynchronous exception to raise */
115115
long thread_id; /* Thread id where this tstate was created */
116116

117+
int trash_delete_nesting;
118+
PyObject *trash_delete_later;
119+
117120
/* XXX signal handlers should also be here */
118121

119122
} PyThreadState;

Lib/test/test_gc.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,15 @@
22
from test.support import (verbose, refcount_test, run_unittest,
33
strip_python_stderr)
44
import sys
5+
import time
56
import gc
67
import weakref
78

9+
try:
10+
import threading
11+
except ImportError:
12+
threading = None
13+
814
### Support code
915
###############################################################################
1016

@@ -328,6 +334,69 @@ def __del__(self):
328334
v = {1: v, 2: Ouch()}
329335
gc.disable()
330336

337+
@unittest.skipUnless(threading, "test meaningless on builds without threads")
338+
def test_trashcan_threads(self):
339+
# Issue #13992: trashcan mechanism should be thread-safe
340+
NESTING = 60
341+
N_THREADS = 2
342+
343+
def sleeper_gen():
344+
"""A generator that releases the GIL when closed or dealloc'ed."""
345+
try:
346+
yield
347+
finally:
348+
time.sleep(0.000001)
349+
350+
class C(list):
351+
# Appending to a list is atomic, which avoids the use of a lock.
352+
inits = []
353+
dels = []
354+
def __init__(self, alist):
355+
self[:] = alist
356+
C.inits.append(None)
357+
def __del__(self):
358+
# This __del__ is called by subtype_dealloc().
359+
C.dels.append(None)
360+
# `g` will release the GIL when garbage-collected. This
361+
# helps assert subtype_dealloc's behaviour when threads
362+
# switch in the middle of it.
363+
g = sleeper_gen()
364+
next(g)
365+
# Now that __del__ is finished, subtype_dealloc will proceed
366+
# to call list_dealloc, which also uses the trashcan mechanism.
367+
368+
def make_nested():
369+
"""Create a sufficiently nested container object so that the
370+
trashcan mechanism is invoked when deallocating it."""
371+
x = C([])
372+
for i in range(NESTING):
373+
x = [C([x])]
374+
del x
375+
376+
def run_thread():
377+
"""Exercise make_nested() in a loop."""
378+
while not exit:
379+
make_nested()
380+
381+
old_switchinterval = sys.getswitchinterval()
382+
sys.setswitchinterval(1e-5)
383+
try:
384+
exit = False
385+
threads = []
386+
for i in range(N_THREADS):
387+
t = threading.Thread(target=run_thread)
388+
threads.append(t)
389+
for t in threads:
390+
t.start()
391+
time.sleep(1.0)
392+
exit = True
393+
for t in threads:
394+
t.join()
395+
finally:
396+
sys.setswitchinterval(old_switchinterval)
397+
gc.collect()
398+
self.assertEqual(len(C.inits), len(C.dels))
399+
331400
def test_boom(self):
332401
class Boom:
333402
def __getattr__(self, someattribute):

Misc/NEWS

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ What's New in Python 3.3.1
1010
Core and Builtins
1111
-----------------
1212

13+
- Issue #13992: The trashcan mechanism is now thread-safe. This eliminates
14+
sporadic crashes in multi-thread programs when several long deallocator
15+
chains ran concurrently and involved subclasses of built-in container
16+
types.
17+
1318
- Issue #15839: Convert SystemErrors in super() to RuntimeErrors.
1419

1520
- Issue #15846: Fix SystemError which happened when using ast.parse in an

Objects/object.c

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1954,6 +1954,18 @@ _PyTrash_deposit_object(PyObject *op)
19541954
_PyTrash_delete_later = op;
19551955
}
19561956

1957+
/* The equivalent API, using per-thread state recursion info */
1958+
void
1959+
_PyTrash_thread_deposit_object(PyObject *op)
1960+
{
1961+
PyThreadState *tstate = PyThreadState_GET();
1962+
assert(PyObject_IS_GC(op));
1963+
assert(_Py_AS_GC(op)->gc.gc_refs == _PyGC_REFS_UNTRACKED);
1964+
assert(op->ob_refcnt == 0);
1965+
_Py_AS_GC(op)->gc.gc_prev = (PyGC_Head *) tstate->trash_delete_later;
1966+
tstate->trash_delete_later = op;
1967+
}
1968+
19571969
/* Dealloccate all the objects in the _PyTrash_delete_later list. Called when
19581970
* the call-stack unwinds again.
19591971
*/
@@ -1980,6 +1992,31 @@ _PyTrash_destroy_chain(void)
19801992
}
19811993
}
19821994

1995+
/* The equivalent API, using per-thread state recursion info */
1996+
void
1997+
_PyTrash_thread_destroy_chain(void)
1998+
{
1999+
PyThreadState *tstate = PyThreadState_GET();
2000+
while (tstate->trash_delete_later) {
2001+
PyObject *op = tstate->trash_delete_later;
2002+
destructor dealloc = Py_TYPE(op)->tp_dealloc;
2003+
2004+
tstate->trash_delete_later =
2005+
(PyObject*) _Py_AS_GC(op)->gc.gc_prev;
2006+
2007+
/* Call the deallocator directly. This used to try to
2008+
* fool Py_DECREF into calling it indirectly, but
2009+
* Py_DECREF was already called on this object, and in
2010+
* assorted non-release builds calling Py_DECREF again ends
2011+
* up distorting allocation statistics.
2012+
*/
2013+
assert(op->ob_refcnt == 0);
2014+
++tstate->trash_delete_nesting;
2015+
(*dealloc)(op);
2016+
--tstate->trash_delete_nesting;
2017+
}
2018+
}
2019+
19832020
#ifndef Py_TRACE_REFS
19842021
/* For Py_LIMITED_API, we need an out-of-line version of _Py_Dealloc.
19852022
Define this here, so we can undefine the macro. */

Objects/typeobject.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -891,6 +891,7 @@ subtype_dealloc(PyObject *self)
891891
{
892892
PyTypeObject *type, *base;
893893
destructor basedealloc;
894+
PyThreadState *tstate = PyThreadState_GET();
894895

895896
/* Extract the type; we expect it to be a heap type */
896897
type = Py_TYPE(self);
@@ -940,8 +941,10 @@ subtype_dealloc(PyObject *self)
940941
/* See explanation at end of function for full disclosure */
941942
PyObject_GC_UnTrack(self);
942943
++_PyTrash_delete_nesting;
944+
++ tstate->trash_delete_nesting;
943945
Py_TRASHCAN_SAFE_BEGIN(self);
944946
--_PyTrash_delete_nesting;
947+
-- tstate->trash_delete_nesting;
945948
/* DO NOT restore GC tracking at this point. weakref callbacks
946949
* (if any, and whether directly here or indirectly in something we
947950
* call) may trigger GC, and if self is tracked at that point, it
@@ -1020,8 +1023,10 @@ subtype_dealloc(PyObject *self)
10201023

10211024
endlabel:
10221025
++_PyTrash_delete_nesting;
1026+
++ tstate->trash_delete_nesting;
10231027
Py_TRASHCAN_SAFE_END(self);
10241028
--_PyTrash_delete_nesting;
1029+
-- tstate->trash_delete_nesting;
10251030

10261031
/* Explanation of the weirdness around the trashcan macros:
10271032

Python/pystate.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,9 @@ new_threadstate(PyInterpreterState *interp, int init)
206206
tstate->c_profileobj = NULL;
207207
tstate->c_traceobj = NULL;
208208

209+
tstate->trash_delete_nesting = 0;
210+
tstate->trash_delete_later = NULL;
211+
209212
if (init)
210213
_PyThreadState_Init(tstate);
211214

0 commit comments

Comments
 (0)