Skip to content

Commit 403aa2d

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. Because of this change, a couple extension modules compiled for 2.7.4 (those which use the trashcan mechanism, despite it being undocumented) will not be loadable by 2.7.3 and earlier. However, extension modules compiled for 2.7.3 and earlier will be loadable by 2.7.4.
1 parent c257169 commit 403aa2d

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
@@ -971,24 +971,33 @@ chain of N deallocations is broken into N / PyTrash_UNWIND_LEVEL pieces,
971971
with the call stack never exceeding a depth of PyTrash_UNWIND_LEVEL.
972972
*/
973973

974+
/* This is the old private API, invoked by the macros before 2.7.4.
975+
Kept for binary compatibility of extensions. */
974976
PyAPI_FUNC(void) _PyTrash_deposit_object(PyObject*);
975977
PyAPI_FUNC(void) _PyTrash_destroy_chain(void);
976978
PyAPI_DATA(int) _PyTrash_delete_nesting;
977979
PyAPI_DATA(PyObject *) _PyTrash_delete_later;
978980

981+
/* The new thread-safe private API, invoked by the macros below. */
982+
PyAPI_FUNC(void) _PyTrash_thread_deposit_object(PyObject*);
983+
PyAPI_FUNC(void) _PyTrash_thread_destroy_chain(void);
984+
979985
#define PyTrash_UNWIND_LEVEL 50
980986

981987
#define Py_TRASHCAN_SAFE_BEGIN(op) \
982-
if (_PyTrash_delete_nesting < PyTrash_UNWIND_LEVEL) { \
983-
++_PyTrash_delete_nesting;
984-
/* The body of the deallocator is here. */
988+
do { \
989+
PyThreadState *_tstate = PyThreadState_GET(); \
990+
if (_tstate->trash_delete_nesting < PyTrash_UNWIND_LEVEL) { \
991+
++_tstate->trash_delete_nesting;
992+
/* The body of the deallocator is here. */
985993
#define Py_TRASHCAN_SAFE_END(op) \
986-
--_PyTrash_delete_nesting; \
987-
if (_PyTrash_delete_later && _PyTrash_delete_nesting <= 0) \
988-
_PyTrash_destroy_chain(); \
989-
} \
990-
else \
991-
_PyTrash_deposit_object((PyObject*)op);
994+
--_tstate->trash_delete_nesting; \
995+
if (_tstate->trash_delete_later && _tstate->trash_delete_nesting <= 0) \
996+
_PyTrash_thread_destroy_chain(); \
997+
} \
998+
else \
999+
_PyTrash_thread_deposit_object((PyObject*)op); \
1000+
} while (0);
9921001

9931002
#ifdef __cplusplus
9941003
}

Include/pystate.h

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

98+
int trash_delete_nesting;
99+
PyObject *trash_delete_later;
100+
98101
/* XXX signal handlers should also be here */
99102

100103
} PyThreadState;

Lib/test/test_gc.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import unittest
22
from test.test_support import verbose, run_unittest
33
import sys
4+
import time
45
import gc
56
import weakref
67

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

@@ -299,6 +305,69 @@ def __del__(self):
299305
v = {1: v, 2: Ouch()}
300306
gc.disable()
301307

308+
@unittest.skipUnless(threading, "test meaningless on builds without threads")
309+
def test_trashcan_threads(self):
310+
# Issue #13992: trashcan mechanism should be thread-safe
311+
NESTING = 60
312+
N_THREADS = 2
313+
314+
def sleeper_gen():
315+
"""A generator that releases the GIL when closed or dealloc'ed."""
316+
try:
317+
yield
318+
finally:
319+
time.sleep(0.000001)
320+
321+
class C(list):
322+
# Appending to a list is atomic, which avoids the use of a lock.
323+
inits = []
324+
dels = []
325+
def __init__(self, alist):
326+
self[:] = alist
327+
C.inits.append(None)
328+
def __del__(self):
329+
# This __del__ is called by subtype_dealloc().
330+
C.dels.append(None)
331+
# `g` will release the GIL when garbage-collected. This
332+
# helps assert subtype_dealloc's behaviour when threads
333+
# switch in the middle of it.
334+
g = sleeper_gen()
335+
next(g)
336+
# Now that __del__ is finished, subtype_dealloc will proceed
337+
# to call list_dealloc, which also uses the trashcan mechanism.
338+
339+
def make_nested():
340+
"""Create a sufficiently nested container object so that the
341+
trashcan mechanism is invoked when deallocating it."""
342+
x = C([])
343+
for i in range(NESTING):
344+
x = [C([x])]
345+
del x
346+
347+
def run_thread():
348+
"""Exercise make_nested() in a loop."""
349+
while not exit:
350+
make_nested()
351+
352+
old_checkinterval = sys.getcheckinterval()
353+
sys.setcheckinterval(3)
354+
try:
355+
exit = False
356+
threads = []
357+
for i in range(N_THREADS):
358+
t = threading.Thread(target=run_thread)
359+
threads.append(t)
360+
for t in threads:
361+
t.start()
362+
time.sleep(1.0)
363+
exit = True
364+
for t in threads:
365+
t.join()
366+
finally:
367+
sys.setcheckinterval(old_checkinterval)
368+
gc.collect()
369+
self.assertEqual(len(C.inits), len(C.dels))
370+
302371
def test_boom(self):
303372
class Boom:
304373
def __getattr__(self, someattribute):

Misc/NEWS

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

12+
- Issue #13992: The trashcan mechanism is now thread-safe. This eliminates
13+
sporadic crashes in multi-thread programs when several long deallocator
14+
chains ran concurrently and involved subclasses of built-in container
15+
types.
16+
1217
- Issue #15801: Make sure mappings passed to '%' formatting are actually
1318
subscriptable.
1419

Objects/object.c

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2428,6 +2428,18 @@ _PyTrash_deposit_object(PyObject *op)
24282428
_PyTrash_delete_later = op;
24292429
}
24302430

2431+
/* The equivalent API, using per-thread state recursion info */
2432+
void
2433+
_PyTrash_thread_deposit_object(PyObject *op)
2434+
{
2435+
PyThreadState *tstate = PyThreadState_GET();
2436+
assert(PyObject_IS_GC(op));
2437+
assert(_Py_AS_GC(op)->gc.gc_refs == _PyGC_REFS_UNTRACKED);
2438+
assert(op->ob_refcnt == 0);
2439+
_Py_AS_GC(op)->gc.gc_prev = (PyGC_Head *) tstate->trash_delete_later;
2440+
tstate->trash_delete_later = op;
2441+
}
2442+
24312443
/* Dealloccate all the objects in the _PyTrash_delete_later list. Called when
24322444
* the call-stack unwinds again.
24332445
*/
@@ -2454,6 +2466,31 @@ _PyTrash_destroy_chain(void)
24542466
}
24552467
}
24562468

2469+
/* The equivalent API, using per-thread state recursion info */
2470+
void
2471+
_PyTrash_thread_destroy_chain(void)
2472+
{
2473+
PyThreadState *tstate = PyThreadState_GET();
2474+
while (tstate->trash_delete_later) {
2475+
PyObject *op = tstate->trash_delete_later;
2476+
destructor dealloc = Py_TYPE(op)->tp_dealloc;
2477+
2478+
tstate->trash_delete_later =
2479+
(PyObject*) _Py_AS_GC(op)->gc.gc_prev;
2480+
2481+
/* Call the deallocator directly. This used to try to
2482+
* fool Py_DECREF into calling it indirectly, but
2483+
* Py_DECREF was already called on this object, and in
2484+
* assorted non-release builds calling Py_DECREF again ends
2485+
* up distorting allocation statistics.
2486+
*/
2487+
assert(op->ob_refcnt == 0);
2488+
++tstate->trash_delete_nesting;
2489+
(*dealloc)(op);
2490+
--tstate->trash_delete_nesting;
2491+
}
2492+
}
2493+
24572494
#ifdef __cplusplus
24582495
}
24592496
#endif

Objects/typeobject.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,7 @@ subtype_dealloc(PyObject *self)
896896
{
897897
PyTypeObject *type, *base;
898898
destructor basedealloc;
899+
PyThreadState *tstate = PyThreadState_GET();
899900

900901
/* Extract the type; we expect it to be a heap type */
901902
type = Py_TYPE(self);
@@ -945,8 +946,10 @@ subtype_dealloc(PyObject *self)
945946
/* See explanation at end of function for full disclosure */
946947
PyObject_GC_UnTrack(self);
947948
++_PyTrash_delete_nesting;
949+
++ tstate->trash_delete_nesting;
948950
Py_TRASHCAN_SAFE_BEGIN(self);
949951
--_PyTrash_delete_nesting;
952+
-- tstate->trash_delete_nesting;
950953
/* DO NOT restore GC tracking at this point. weakref callbacks
951954
* (if any, and whether directly here or indirectly in something we
952955
* call) may trigger GC, and if self is tracked at that point, it
@@ -1025,8 +1028,10 @@ subtype_dealloc(PyObject *self)
10251028

10261029
endlabel:
10271030
++_PyTrash_delete_nesting;
1031+
++ tstate->trash_delete_nesting;
10281032
Py_TRASHCAN_SAFE_END(self);
10291033
--_PyTrash_delete_nesting;
1034+
-- tstate->trash_delete_nesting;
10301035

10311036
/* Explanation of the weirdness around the trashcan macros:
10321037

Python/pystate.c

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

195+
tstate->trash_delete_nesting = 0;
196+
tstate->trash_delete_later = NULL;
197+
195198
if (init)
196199
_PyThreadState_Init(tstate);
197200

0 commit comments

Comments
 (0)