mirror of
https://github.com/python/cpython.git
synced 2025-09-26 18:29:57 +00:00
Reworked move_finalizer_reachable() to create two distinct lists:
externally unreachable objects with finalizers, and externally unreachable objects without finalizers reachable from such objects. This allows us to call has_finalizer() at most once per object, and so limit the pain of nasty getattr hooks. This fixes the failing "boom 2" example Jeremy posted (a non-printing variant of which is now part of test_gc), via never triggering the nasty part of its __getattr__ method.
This commit is contained in:
parent
f6ae7a43eb
commit
bf384c256e
2 changed files with 89 additions and 35 deletions
|
@ -253,21 +253,21 @@ def test_trashcan():
|
||||||
v = {1: v, 2: Ouch()}
|
v = {1: v, 2: Ouch()}
|
||||||
gc.disable()
|
gc.disable()
|
||||||
|
|
||||||
class C:
|
class Boom:
|
||||||
def __getattr__(self, someattribute):
|
def __getattr__(self, someattribute):
|
||||||
del self.attr
|
del self.attr
|
||||||
raise AttributeError
|
raise AttributeError
|
||||||
|
|
||||||
def test_boom():
|
def test_boom():
|
||||||
a = C()
|
a = Boom()
|
||||||
b = C()
|
b = Boom()
|
||||||
a.attr = b
|
a.attr = b
|
||||||
b.attr = a
|
b.attr = a
|
||||||
|
|
||||||
gc.collect()
|
gc.collect()
|
||||||
garbagelen = len(gc.garbage)
|
garbagelen = len(gc.garbage)
|
||||||
del a, b
|
del a, b
|
||||||
# a<->b are in a trash cycle now. Collection will invoke C.__getattr__
|
# a<->b are in a trash cycle now. Collection will invoke Boom.__getattr__
|
||||||
# (to see whether a and b have __del__ methods), and __getattr__ deletes
|
# (to see whether a and b have __del__ methods), and __getattr__ deletes
|
||||||
# the internal "attr" attributes as a side effect. That causes the
|
# the internal "attr" attributes as a side effect. That causes the
|
||||||
# trash cycle to get reclaimed via refcounts falling to 0, thus mutating
|
# trash cycle to get reclaimed via refcounts falling to 0, thus mutating
|
||||||
|
@ -276,6 +276,33 @@ def test_boom():
|
||||||
expect(gc.collect(), 0, "boom")
|
expect(gc.collect(), 0, "boom")
|
||||||
expect(len(gc.garbage), garbagelen, "boom")
|
expect(len(gc.garbage), garbagelen, "boom")
|
||||||
|
|
||||||
|
class Boom2:
|
||||||
|
def __init__(self):
|
||||||
|
self.x = 0
|
||||||
|
|
||||||
|
def __getattr__(self, someattribute):
|
||||||
|
self.x += 1
|
||||||
|
if self.x > 1:
|
||||||
|
del self.attr
|
||||||
|
raise AttributeError
|
||||||
|
|
||||||
|
def test_boom2():
|
||||||
|
a = Boom2()
|
||||||
|
b = Boom2()
|
||||||
|
a.attr = b
|
||||||
|
b.attr = a
|
||||||
|
|
||||||
|
gc.collect()
|
||||||
|
garbagelen = len(gc.garbage)
|
||||||
|
del a, b
|
||||||
|
# Much like test_boom(), except that __getattr__ doesn't break the
|
||||||
|
# cycle until the second time gc checks for __del__. As of 2.3b1,
|
||||||
|
# there isn't a second time, so this simply cleans up the trash cycle.
|
||||||
|
# We expect a, b, a.__dict__ and b.__dict__ (4 objects) to get reclaimed
|
||||||
|
# this way.
|
||||||
|
expect(gc.collect(), 4, "boom2")
|
||||||
|
expect(len(gc.garbage), garbagelen, "boom2")
|
||||||
|
|
||||||
def test_all():
|
def test_all():
|
||||||
gc.collect() # Delete 2nd generation garbage
|
gc.collect() # Delete 2nd generation garbage
|
||||||
run_test("lists", test_list)
|
run_test("lists", test_list)
|
||||||
|
@ -295,6 +322,7 @@ def test_all():
|
||||||
run_test("saveall", test_saveall)
|
run_test("saveall", test_saveall)
|
||||||
run_test("trashcan", test_trashcan)
|
run_test("trashcan", test_trashcan)
|
||||||
run_test("boom", test_boom)
|
run_test("boom", test_boom)
|
||||||
|
run_test("boom2", test_boom2)
|
||||||
|
|
||||||
def test():
|
def test():
|
||||||
if verbose:
|
if verbose:
|
||||||
|
|
|
@ -51,13 +51,13 @@ PyGC_Head *_PyGC_generation0 = GEN_HEAD(0);
|
||||||
static int enabled = 1; /* automatic collection enabled? */
|
static int enabled = 1; /* automatic collection enabled? */
|
||||||
|
|
||||||
/* true if we are currently running the collector */
|
/* true if we are currently running the collector */
|
||||||
static int collecting;
|
static int collecting = 0;
|
||||||
|
|
||||||
/* list of uncollectable objects */
|
/* list of uncollectable objects */
|
||||||
static PyObject *garbage;
|
static PyObject *garbage = NULL;
|
||||||
|
|
||||||
/* Python string to use if unhandled exception occurs */
|
/* Python string to use if unhandled exception occurs */
|
||||||
static PyObject *gc_str;
|
static PyObject *gc_str = NULL;
|
||||||
|
|
||||||
/* Python string used to look for __del__ attribute. */
|
/* Python string used to look for __del__ attribute. */
|
||||||
static PyObject *delstr = NULL;
|
static PyObject *delstr = NULL;
|
||||||
|
@ -412,19 +412,20 @@ visit_move(PyObject *op, PyGC_Head *tolist)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Move objects that are reachable from finalizers, from the unreachable set
|
/* Move objects that are reachable from finalizers, from the unreachable set
|
||||||
* into the finalizers set.
|
* into the reachable_from_finalizers set.
|
||||||
*/
|
*/
|
||||||
static void
|
static void
|
||||||
move_finalizer_reachable(PyGC_Head *finalizers)
|
move_finalizer_reachable(PyGC_Head *finalizers,
|
||||||
|
PyGC_Head *reachable_from_finalizers)
|
||||||
{
|
{
|
||||||
traverseproc traverse;
|
traverseproc traverse;
|
||||||
PyGC_Head *gc = finalizers->gc.gc_next;
|
PyGC_Head *gc = finalizers->gc.gc_next;
|
||||||
for (; gc != finalizers; gc=gc->gc.gc_next) {
|
for (; gc != finalizers; gc = gc->gc.gc_next) {
|
||||||
/* careful, finalizers list is growing here */
|
/* Note that the finalizers list may grow during this. */
|
||||||
traverse = FROM_GC(gc)->ob_type->tp_traverse;
|
traverse = FROM_GC(gc)->ob_type->tp_traverse;
|
||||||
(void) traverse(FROM_GC(gc),
|
(void) traverse(FROM_GC(gc),
|
||||||
(visitproc)visit_move,
|
(visitproc)visit_move,
|
||||||
(void *)finalizers);
|
(void *)reachable_from_finalizers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -454,19 +455,25 @@ debug_cycle(char *msg, PyObject *op)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Handle uncollectable garbage (cycles with finalizers). */
|
/* Handle uncollectable garbage (cycles with finalizers, and stuff reachable
|
||||||
|
* only from such cycles).
|
||||||
|
*/
|
||||||
static void
|
static void
|
||||||
handle_finalizers(PyGC_Head *finalizers, PyGC_Head *old)
|
handle_finalizers(PyGC_Head *finalizers, PyGC_Head *old, int hasfinalizer)
|
||||||
{
|
{
|
||||||
PyGC_Head *gc;
|
PyGC_Head *gc;
|
||||||
if (garbage == NULL) {
|
if (garbage == NULL) {
|
||||||
garbage = PyList_New(0);
|
garbage = PyList_New(0);
|
||||||
|
if (garbage == NULL)
|
||||||
|
Py_FatalError("gc couldn't create gc.garbage list");
|
||||||
}
|
}
|
||||||
for (gc = finalizers->gc.gc_next; gc != finalizers;
|
for (gc = finalizers->gc.gc_next;
|
||||||
gc = finalizers->gc.gc_next) {
|
gc != finalizers;
|
||||||
|
gc = finalizers->gc.gc_next) {
|
||||||
PyObject *op = FROM_GC(gc);
|
PyObject *op = FROM_GC(gc);
|
||||||
/* XXX has_finalizer() is not safe here. */
|
|
||||||
if ((debug & DEBUG_SAVEALL) || has_finalizer(op)) {
|
assert(IS_REACHABLE(op));
|
||||||
|
if ((debug & DEBUG_SAVEALL) || hasfinalizer) {
|
||||||
/* If SAVEALL is not set then just append objects with
|
/* If SAVEALL is not set then just append objects with
|
||||||
* finalizers to the list of garbage. All objects in
|
* finalizers to the list of garbage. All objects in
|
||||||
* the finalizers list are reachable from those
|
* the finalizers list are reachable from those
|
||||||
|
@ -474,8 +481,6 @@ handle_finalizers(PyGC_Head *finalizers, PyGC_Head *old)
|
||||||
*/
|
*/
|
||||||
PyList_Append(garbage, op);
|
PyList_Append(garbage, op);
|
||||||
}
|
}
|
||||||
/* object is now reachable again */
|
|
||||||
assert(IS_REACHABLE(op));
|
|
||||||
gc_list_remove(gc);
|
gc_list_remove(gc);
|
||||||
gc_list_append(gc, old);
|
gc_list_append(gc, old);
|
||||||
}
|
}
|
||||||
|
@ -527,6 +532,7 @@ collect(int generation)
|
||||||
PyGC_Head unreachable;
|
PyGC_Head unreachable;
|
||||||
PyGC_Head collectable;
|
PyGC_Head collectable;
|
||||||
PyGC_Head finalizers;
|
PyGC_Head finalizers;
|
||||||
|
PyGC_Head reachable_from_finalizers;
|
||||||
PyGC_Head *gc;
|
PyGC_Head *gc;
|
||||||
|
|
||||||
if (delstr == NULL) {
|
if (delstr == NULL) {
|
||||||
|
@ -589,16 +595,27 @@ collect(int generation)
|
||||||
* care not to create such things. For Python, finalizers means
|
* care not to create such things. For Python, finalizers means
|
||||||
* instance objects with __del__ methods.
|
* instance objects with __del__ methods.
|
||||||
*
|
*
|
||||||
* Move each object into the collectable set or the finalizers set.
|
* Move each unreachable object into the collectable set or the
|
||||||
* Because we need to check for __del__ methods on instances of
|
* finalizers set. Because we need to check for __del__ methods on
|
||||||
* classic classes, arbitrary Python code may get executed by
|
* instances of classic classes, arbitrary Python code may get
|
||||||
* getattr hooks: that may resurrect or deallocate (via refcount
|
* executed by getattr hooks: that may resurrect or deallocate (via
|
||||||
* falling to 0) unreachable objects, so this is very delicate.
|
* refcount falling to 0) unreachable objects, so this is very
|
||||||
|
* delicate.
|
||||||
*/
|
*/
|
||||||
gc_list_init(&collectable);
|
gc_list_init(&collectable);
|
||||||
gc_list_init(&finalizers);
|
gc_list_init(&finalizers);
|
||||||
move_finalizers(&unreachable, &collectable, &finalizers);
|
move_finalizers(&unreachable, &collectable, &finalizers);
|
||||||
move_finalizer_reachable(&finalizers);
|
/* finalizers contains the unreachable objects with a finalizer;
|
||||||
|
* unreachable objects reachable only *from* those are also
|
||||||
|
* uncollectable; we move those into a separate list
|
||||||
|
* (reachable_from_finalizers) so we don't have to do the dangerous
|
||||||
|
* has_finalizer() test again later.
|
||||||
|
*/
|
||||||
|
gc_list_init(&reachable_from_finalizers);
|
||||||
|
move_finalizer_reachable(&finalizers, &reachable_from_finalizers);
|
||||||
|
/* And move everything only reachable from the reachable stuff. */
|
||||||
|
move_finalizer_reachable(&reachable_from_finalizers,
|
||||||
|
&reachable_from_finalizers);
|
||||||
|
|
||||||
/* Collect statistics on collectable objects found and print
|
/* Collect statistics on collectable objects found and print
|
||||||
* debugging information. */
|
* debugging information. */
|
||||||
|
@ -610,18 +627,25 @@ collect(int generation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* Call tp_clear on objects in the collectable set. This will cause
|
/* Call tp_clear on objects in the collectable set. This will cause
|
||||||
* the reference cycles to be broken. It may also cause some objects in
|
* the reference cycles to be broken. It may also cause some objects
|
||||||
* finalizers to be freed */
|
* in finalizers and/or reachable_from_finalizers to be freed */
|
||||||
delete_garbage(&collectable, old);
|
delete_garbage(&collectable, old);
|
||||||
|
|
||||||
/* Collect statistics on uncollectable objects found and print
|
/* Collect statistics on uncollectable objects found and print
|
||||||
* debugging information. */
|
* debugging information. */
|
||||||
for (gc = finalizers.gc.gc_next; gc != &finalizers;
|
for (gc = finalizers.gc.gc_next;
|
||||||
gc = gc->gc.gc_next) {
|
gc != &finalizers;
|
||||||
|
gc = gc->gc.gc_next) {
|
||||||
n++;
|
n++;
|
||||||
if (debug & DEBUG_UNCOLLECTABLE) {
|
if (debug & DEBUG_UNCOLLECTABLE)
|
||||||
|
debug_cycle("uncollectable", FROM_GC(gc));
|
||||||
|
}
|
||||||
|
for (gc = reachable_from_finalizers.gc.gc_next;
|
||||||
|
gc != &reachable_from_finalizers;
|
||||||
|
gc = gc->gc.gc_next) {
|
||||||
|
n++;
|
||||||
|
if (debug & DEBUG_UNCOLLECTABLE)
|
||||||
debug_cycle("uncollectable", FROM_GC(gc));
|
debug_cycle("uncollectable", FROM_GC(gc));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (debug & DEBUG_STATS) {
|
if (debug & DEBUG_STATS) {
|
||||||
if (m == 0 && n == 0) {
|
if (m == 0 && n == 0) {
|
||||||
|
@ -636,8 +660,10 @@ collect(int generation)
|
||||||
|
|
||||||
/* Append instances in the uncollectable set to a Python
|
/* Append instances in the uncollectable set to a Python
|
||||||
* reachable list of garbage. The programmer has to deal with
|
* reachable list of garbage. The programmer has to deal with
|
||||||
* this if they insist on creating this type of structure. */
|
* this if they insist on creating this type of structure.
|
||||||
handle_finalizers(&finalizers, old);
|
*/
|
||||||
|
handle_finalizers(&finalizers, old, 1);
|
||||||
|
handle_finalizers(&reachable_from_finalizers, old, 0);
|
||||||
|
|
||||||
if (PyErr_Occurred()) {
|
if (PyErr_Occurred()) {
|
||||||
if (gc_str == NULL) {
|
if (gc_str == NULL) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue