Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 56cd62c

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 3.2.4 (those which use the trashcan mechanism, despite it being undocumented) will not be loadable by 3.2.3 and earlier. However, extension modules compiled for 3.2.3 and earlier will be loadable by 3.2.4.
1 parent 1d85745 commit 56cd62c

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

914+
/* This is the old private API, invoked by the macros before 3.2.4.
915+
Kept for binary compatibility of extensions. */
914916
PyAPI_FUNC(void) _PyTrash_deposit_object(PyObject*);
915917
PyAPI_FUNC(void) _PyTrash_destroy_chain(void);
916918
PyAPI_DATA(int) _PyTrash_delete_nesting;
917919
PyAPI_DATA(PyObject *) _PyTrash_delete_later;
918920

921+
/* The new thread-safe private API, invoked by the macros below. */
922+
PyAPI_FUNC(void) _PyTrash_thread_deposit_object(PyObject*);
923+
PyAPI_FUNC(void) _PyTrash_thread_destroy_chain(void);
924+
919925
#define PyTrash_UNWIND_LEVEL 50
920926

921927
#define Py_TRASHCAN_SAFE_BEGIN(op) \
922-
if (_PyTrash_delete_nesting < PyTrash_UNWIND_LEVEL) { \
923-
++_PyTrash_delete_nesting;
924-
/* The body of the deallocator is here. */
928+
do { \
929+
PyThreadState *_tstate = PyThreadState_GET(); \
930+
if (_tstate->trash_delete_nesting < PyTrash_UNWIND_LEVEL) { \
931+
++_tstate->trash_delete_nesting;
932+
/* The body of the deallocator is here. */
925933
#define Py_TRASHCAN_SAFE_END(op) \
926-
--_PyTrash_delete_nesting; \
927-
if (_PyTrash_delete_later && _PyTrash_delete_nesting <= 0) \
928-
_PyTrash_destroy_chain(); \
929-
} \
930-
else \
931-
_PyTrash_deposit_object((PyObject*)op);
934+
--_tstate->trash_delete_nesting; \
935+
if (_tstate->trash_delete_later && _tstate->trash_delete_nesting <= 0) \
936+
_PyTrash_thread_destroy_chain(); \
937+
} \
938+
else \
939+
_PyTrash_thread_deposit_object((PyObject*)op); \
940+
} while (0);
932941

933942
#ifdef __cplusplus
934943
}

Include/pystate.h

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

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

118121
} 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.support import verbose, run_unittest, strip_python_stderr
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

@@ -310,6 +316,69 @@ def __del__(self):
310316
v = {1: v, 2: Ouch()}
311317
gc.disable()
312318

319+
@unittest.skipUnless(threading, "test meaningless on builds without threads")
320+
def test_trashcan_threads(self):
321+
# Issue #13992: trashcan mechanism should be thread-safe
322+
NESTING = 60
323+
N_THREADS = 2
324+
325+
def sleeper_gen():
326+
"""A generator that releases the GIL when closed or dealloc'ed."""
327+
try:
328+
yield
329+
finally:
330+
time.sleep(0.000001)
331+
332+
class C(list):
333+
# Appending to a list is atomic, which avoids the use of a lock.
334+
inits = []
335+
dels = []
336+
def __init__(self, alist):
337+
self[:] = alist
338+
C.inits.append(None)
339+
def __del__(self):
340+
# This __del__ is called by subtype_dealloc().
341+
C.dels.append(None)
342+
# `g` will release the GIL when garbage-collected. This
343+
# helps assert subtype_dealloc's behaviour when threads
344+
# switch in the middle of it.
345+
g = sleeper_gen()
346+
next(g)
347+
# Now that __del__ is finished, subtype_dealloc will proceed
348+
# to call list_dealloc, which also uses the trashcan mechanism.
349+
350+
def make_nested():
351+
"""Create a sufficiently nested container object so that the
352+
trashcan mechanism is invoked when deallocating it."""
353+
x = C([])
354+
for i in range(NESTING):
355+
x = [C([x])]
356+
del x
357+
358+
def run_thread():
359+
"""Exercise make_nested() in a loop."""
360+
while not exit:
361+
make_nested()
362+
363+
old_switchinterval = sys.getswitchinterval()
364+
sys.setswitchinterval(1e-5)
365+
try:
366+
exit = False
367+
threads = []
368+
for i in range(N_THREADS):
369+
t = threading.Thread(target=run_thread)
370+
threads.append(t)
371+
for t in threads:
372+
t.start()
373+
time.sleep(1.0)
374+
exit = True
375+
for t in threads:
376+
t.join()
377+
finally:
378+
sys.setswitchinterval(old_switchinterval)
379+
gc.collect()
380+
self.assertEqual(len(C.inits), len(C.dels))
381+
313382
def test_boom(self):
314383
class Boom:
315384
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.2.4
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 #15846: Fix SystemError which happened when using ast.parse in an
1419
exception handler on code with syntax errors.
1520

Objects/object.c

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1876,6 +1876,18 @@ _PyTrash_deposit_object(PyObject *op)
18761876
_PyTrash_delete_later = op;
18771877
}
18781878

1879+
/* The equivalent API, using per-thread state recursion info */
1880+
void
1881+
_PyTrash_thread_deposit_object(PyObject *op)
1882+
{
1883+
PyThreadState *tstate = PyThreadState_GET();
1884+
assert(PyObject_IS_GC(op));
1885+
assert(_Py_AS_GC(op)->gc.gc_refs == _PyGC_REFS_UNTRACKED);
1886+
assert(op->ob_refcnt == 0);
1887+
_Py_AS_GC(op)->gc.gc_prev = (PyGC_Head *) tstate->trash_delete_later;
1888+
tstate->trash_delete_later = op;
1889+
}
1890+
18791891
/* Dealloccate all the objects in the _PyTrash_delete_later list. Called when
18801892
* the call-stack unwinds again.
18811893
*/
@@ -1902,6 +1914,31 @@ _PyTrash_destroy_chain(void)
19021914
}
19031915
}
19041916

1917+
/* The equivalent API, using per-thread state recursion info */
1918+
void
1919+
_PyTrash_thread_destroy_chain(void)
1920+
{
1921+
PyThreadState *tstate = PyThreadState_GET();
1922+
while (tstate->trash_delete_later) {
1923+
PyObject *op = tstate->trash_delete_later;
1924+
destructor dealloc = Py_TYPE(op)->tp_dealloc;
1925+
1926+
tstate->trash_delete_later =
1927+
(PyObject*) _Py_AS_GC(op)->gc.gc_prev;
1928+
1929+
/* Call the deallocator directly. This used to try to
1930+
* fool Py_DECREF into calling it indirectly, but
1931+
* Py_DECREF was already called on this object, and in
1932+
* assorted non-release builds calling Py_DECREF again ends
1933+
* up distorting allocation statistics.
1934+
*/
1935+
assert(op->ob_refcnt == 0);
1936+
++tstate->trash_delete_nesting;
1937+
(*dealloc)(op);
1938+
--tstate->trash_delete_nesting;
1939+
}
1940+
}
1941+
19051942
#ifndef Py_TRACE_REFS
19061943
/* For Py_LIMITED_API, we need an out-of-line version of _Py_Dealloc.
19071944
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
@@ -847,6 +847,7 @@ subtype_dealloc(PyObject *self)
847847
{
848848
PyTypeObject *type, *base;
849849
destructor basedealloc;
850+
PyThreadState *tstate = PyThreadState_GET();
850851

851852
/* Extract the type; we expect it to be a heap type */
852853
type = Py_TYPE(self);
@@ -896,8 +897,10 @@ subtype_dealloc(PyObject *self)
896897
/* See explanation at end of function for full disclosure */
897898
PyObject_GC_UnTrack(self);
898899
++_PyTrash_delete_nesting;
900+
++ tstate->trash_delete_nesting;
899901
Py_TRASHCAN_SAFE_BEGIN(self);
900902
--_PyTrash_delete_nesting;
903+
-- tstate->trash_delete_nesting;
901904
/* DO NOT restore GC tracking at this point. weakref callbacks
902905
* (if any, and whether directly here or indirectly in something we
903906
* call) may trigger GC, and if self is tracked at that point, it
@@ -976,8 +979,10 @@ subtype_dealloc(PyObject *self)
976979

977980
endlabel:
978981
++_PyTrash_delete_nesting;
982+
++ tstate->trash_delete_nesting;
979983
Py_TRASHCAN_SAFE_END(self);
980984
--_PyTrash_delete_nesting;
985+
-- tstate->trash_delete_nesting;
981986

982987
/* Explanation of the weirdness around the trashcan macros:
983988

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)