From 1444ab095c0c8461e6444d0b12be477310e93b73 Mon Sep 17 00:00:00 2001 From: Anselm Kruis Date: Thu, 27 Dec 2018 21:11:34 +0100 Subject: [PATCH 1/4] Stackless issue #192: use tp_finalize for complex finalisation code Refactor the finalisation code of stackless.tasklet and stackless.channel. - For tasklets, use a PEP 442 finalizer; - for channels a simple tp_clear function is sufficient. Like every PEP 442 other finalizer, the new finalizer runs only once for each tasklet. --- Stackless/changelog.txt | 6 ++++ Stackless/core/stackless_impl.h | 2 -- Stackless/core/stackless_util.c | 61 -------------------------------- Stackless/module/channelobject.c | 26 ++++---------- Stackless/module/taskletobject.c | 49 +++++++++++++++++++------ 5 files changed, 52 insertions(+), 92 deletions(-) diff --git a/Stackless/changelog.txt b/Stackless/changelog.txt index 862586825a555fd..3a9e7a3a595be03 100644 --- a/Stackless/changelog.txt +++ b/Stackless/changelog.txt @@ -9,6 +9,12 @@ What's New in Stackless 3.X.X? *Release date: 20XX-XX-XX* +- https://github.com/stackless-dev/stackless/issues/192 + Use the PEP 442 mechanic to finalize a tasklet instead of a arcane logic in + tp_dealloc. The new code calls the finalizer only once. The finalizer tries + to kill the tasklet, as long as it has an unfinished C-state. This change + brings the behavior more in line with generators. + - https://github.com/stackless-dev/stackless/issues/193 Fix the macros PyTasklet_CheckExact(op) and PyChannel_CheckExact(op). Previously they contained a reference to a non existing variable. diff --git a/Stackless/core/stackless_impl.h b/Stackless/core/stackless_impl.h index 69656f10c07a90c..d548c6da6835e8d 100644 --- a/Stackless/core/stackless_impl.h +++ b/Stackless/core/stackless_impl.h @@ -737,8 +737,6 @@ int slp_return_wrapper_hard(PyObject *retval); int slp_int_wrapper(PyObject *retval); int slp_current_wrapper(int(*func)(PyTaskletObject*), PyTaskletObject *task); -int slp_resurrect_and_kill(PyObject *self, - void(*killer)(PyObject *)); /* stackless pickling support */ int slp_async_gen_init_hooks(PyAsyncGenObject *o); diff --git a/Stackless/core/stackless_util.c b/Stackless/core/stackless_util.c index 79135644254d64f..c2672767db811c2 100644 --- a/Stackless/core/stackless_util.c +++ b/Stackless/core/stackless_util.c @@ -122,67 +122,6 @@ slp_current_wrapper( int(*func)(PyTaskletObject*), PyTaskletObject *task ) return ret; } -int -slp_resurrect_and_kill(PyObject *self, void(*killer)(PyObject *)) -{ - /* modelled after typeobject.c's slot_tp_del */ - - PyObject *error_type, *error_value, *error_traceback; - - /* this is different from typeobject.c's slot_tp_del: our callers already - called PyObject_GC_UnTrack(self) */ - assert(PyObject_IS_GC(self) && ! _PyObject_GC_IS_TRACKED(self)); - - /* Temporarily resurrect the object. */ - assert(Py_REFCNT(self) == 0); - Py_REFCNT(self) = 1; - - /* Save the current exception, if any. */ - PyErr_Fetch(&error_type, &error_value, &error_traceback); - - killer(self); - - /* Restore the saved exception. */ - PyErr_Restore(error_type, error_value, error_traceback); - - /* Undo the temporary resurrection; can't use DECREF here, it would - * cause a recursive call. - */ - assert(Py_REFCNT(self) > 0); - if (--Py_REFCNT(self) == 0) - return 0; /* this is the normal path out */ - - /* __del__ resurrected it! Make it look like the original Py_DECREF - * never happened. - */ - { - Py_ssize_t refcnt = Py_REFCNT(self); - _Py_NewReference(self); - Py_REFCNT(self) = refcnt; - } - /* If Py_REF_DEBUG, _Py_NewReference bumped _Py_RefTotal, so - * we need to undo that. */ - _Py_DEC_REFTOTAL; - /* If Py_TRACE_REFS, _Py_NewReference re-added self to the object - * chain, so no more to do there. - * If COUNT_ALLOCS, the original decref bumped tp_frees, and - * _Py_NewReference bumped tp_allocs: both of those need to be - * undone. - */ -#ifdef COUNT_ALLOCS - --Py_TYPE(self)->tp_frees; - --Py_TYPE(self)->tp_allocs; -#endif - /* This code copied from iobase_dealloc() (in 3.0) */ - /* When called from a heap type's dealloc, the type will be - decref'ed on return (see e.g. subtype_dealloc in typeobject.c). - This will undo that, thus preventing a crash. But such a type - _will_ have had its dict and slots cleared. */ - if (PyType_HasFeature(Py_TYPE(self), Py_TPFLAGS_HEAPTYPE)) - Py_INCREF(Py_TYPE(self)); - - return -1; -} /* * A thread id is either an unsigned long or the special value -1. diff --git a/Stackless/module/channelobject.c b/Stackless/module/channelobject.c index acdbffdb28aa2a8..06daa594f4aa0d4 100644 --- a/Stackless/module/channelobject.c +++ b/Stackless/module/channelobject.c @@ -11,9 +11,6 @@ #include "core/stackless_impl.h" #include "channelobject.h" -static void -channel_remove_all(PyObject *ob); - Py_LOCAL_INLINE(PyChannelFlagStruc) channel_flags_from_integer(int flags) { #if defined(SLP_USE_NATIVE_BITFIELD_LAYOUT) && SLP_USE_NATIVE_BITFIELD_LAYOUT @@ -62,15 +59,9 @@ channel_traverse(PyChannelObject *ch, visitproc visit, void *arg) static void channel_clear(PyObject *ob) -{ - /* this function does nothing but decref, so it's safe to use */ - channel_remove_all(ob); -} - -static void -channel_remove_all(PyObject *ob) { PyChannelObject *ch = (PyChannelObject *) ob; + assert(PyChannel_Check(ob)); /* * remove all tasklets and hope they will die. @@ -78,7 +69,9 @@ channel_remove_all(PyObject *ob) * during tasklet deallocation, so we don't even know * if it will have the same direction. :-/ */ + ch->flags.closing = 1; while (ch->balance) { + /* this function does nothing but setting task->flags.blocked = 0, so it's safe to use */ ob = (PyObject *) slp_channel_remove(ch, NULL, NULL, NULL); Py_DECREF(ob); } @@ -88,15 +81,10 @@ static void channel_dealloc(PyObject *ob) { PyChannelObject *ch = (PyChannelObject *) ob; - PyObject_GC_UnTrack(ob); - if (ch->balance) { - if (slp_resurrect_and_kill(ob, channel_remove_all)) { - /* the beast has grown new references */ - PyObject_GC_Track(ob); - return; - } - } + + PyObject_GC_UnTrack(ob); + channel_clear(ob); if (ch->chan_weakreflist != NULL) PyObject_ClearWeakRefs((PyObject *)ch); Py_TYPE(ob)->tp_free(ob); @@ -1152,7 +1140,7 @@ channel_setstate(PyObject *self, PyObject *args) &PyList_Type, &lis)) return NULL; - channel_remove_all((PyObject *) ch); + channel_clear((PyObject *) ch); n = PyList_GET_SIZE(lis); ch->flags = channel_flags_from_integer(flags); dir = balance > 0 ? 1 : -1; diff --git a/Stackless/module/taskletobject.c b/Stackless/module/taskletobject.c index 599ca1242d85a87..1aaed550f3b8d92 100644 --- a/Stackless/module/taskletobject.c +++ b/Stackless/module/taskletobject.c @@ -249,24 +249,43 @@ exc_state_clear(_PyErr_StackItem *exc_state) } static void -tasklet_dealloc(PyTaskletObject *t) +tasklet_finalize(PyObject *self) { - PyObject_GC_UnTrack(t); + PyTaskletObject *t; + PyObject *error_type, *error_value, *error_traceback; + + assert(PyTasklet_Check(self)); + t = (PyTaskletObject *)self; + + /* Save the current exception, if any. */ + PyErr_Fetch(&error_type, &error_value, &error_traceback); + if (tasklet_has_c_stack_and_thread(t)) { /* * we want to cleanly kill the tasklet in the case it - * was forgotten. One way would be to resurrect it, - * but this is quite ugly with many ifdefs, see - * classobject/typeobject. - * Well, we do it. + * was forgotten. + */ + kill_finally(self); + } + + /* Restore the saved exception. */ + PyErr_Restore(error_type, error_value, error_traceback); +} + +static void +tasklet_dealloc(PyTaskletObject *t) +{ + if (PyTasklet_CheckExact(t)) { + /* When ob is subclass of stackless.tasklet, finalizer is called from + * subtype_dealloc. */ - if (slp_resurrect_and_kill((PyObject *) t, kill_finally)) { - /* the beast has grown new references */ - PyObject_GC_Track(t); + if (PyObject_CallFinalizerFromDealloc((PyObject *)t) < 0) { + // resurrected. return; } } + PyObject_GC_UnTrack(t); tasklet_clear_frames(t); if (t->tsk_weakreflist != NULL) PyObject_ClearWeakRefs((PyObject *)t); @@ -2339,7 +2358,8 @@ PyTypeObject PyTasklet_Type = { PyObject_GenericSetAttr, /* tp_setattro */ 0, /* tp_as_buffer */ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | - Py_TPFLAGS_BASETYPE, /* tp_flags */ + Py_TPFLAGS_BASETYPE | + Py_TPFLAGS_HAVE_FINALIZE, /* tp_flags */ tasklet__doc__, /* tp_doc */ (traverseproc)tasklet_traverse, /* tp_traverse */ (inquiry) tasklet_clear, /* tp_clear */ @@ -2359,5 +2379,14 @@ PyTypeObject PyTasklet_Type = { 0, /* tp_alloc */ tasklet_new, /* tp_new */ PyObject_GC_Del, /* tp_free */ + 0, /* tp_is_gc */ + 0, /* tp_bases */ + 0, /* tp_mro */ + 0, /* tp_cache */ + 0, /* tp_subclasses */ + 0, /* tp_weaklist */ + 0, /* tp_del */ + 0, /* tp_version_tag */ + tasklet_finalize, /* tp_finalize */ }; #endif From 637f9d3bbee96b3b46452cfe132d7204260214cb Mon Sep 17 00:00:00 2001 From: Anselm Kruis Date: Thu, 27 Dec 2018 21:25:19 +0100 Subject: [PATCH 2/4] Stackless issue #192: kill a tasklet at most 10 times In order to prevent an endless loop in the tasklet-finalizer, try at most 10 times to kill a tasklet. --- Stackless/changelog.txt | 4 ++-- Stackless/module/taskletobject.c | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Stackless/changelog.txt b/Stackless/changelog.txt index 3a9e7a3a595be03..2e0ecf9c013b35d 100644 --- a/Stackless/changelog.txt +++ b/Stackless/changelog.txt @@ -12,8 +12,8 @@ What's New in Stackless 3.X.X? - https://github.com/stackless-dev/stackless/issues/192 Use the PEP 442 mechanic to finalize a tasklet instead of a arcane logic in tp_dealloc. The new code calls the finalizer only once. The finalizer tries - to kill the tasklet, as long as it has an unfinished C-state. This change - brings the behavior more in line with generators. + to kill the tasklet, as long as it has an unfinished C-state but at most 10- + times. This change brings the behavior more in line with generators. - https://github.com/stackless-dev/stackless/issues/193 Fix the macros PyTasklet_CheckExact(op) and PyChannel_CheckExact(op). diff --git a/Stackless/module/taskletobject.c b/Stackless/module/taskletobject.c index 1aaed550f3b8d92..a52bacde7ac3bef 100644 --- a/Stackless/module/taskletobject.c +++ b/Stackless/module/taskletobject.c @@ -204,7 +204,7 @@ tasklet_clear(PyTaskletObject *t) * afford that it will not be dead afterwards. * Reason: When clearing or resurrecting and killing, the * tasklet is in fact already dead, and the only case that - * could revive it was that __del_ was defined. + * could revive it was that __del__ was defined. * But in the context of __del__, we can't do anything but rely * on proper destruction, since nobody will listen to an exception. */ @@ -215,6 +215,7 @@ kill_finally (PyObject *ob) PyThreadState *ts = PyThreadState_GET(); PyTaskletObject *self = (PyTaskletObject *) ob; int is_mine = ts == self->cstate->tstate; + int i; /* this could happen if we have a refcount bug, so catch it here. assert(self != ts->st.current); @@ -224,7 +225,7 @@ kill_finally (PyObject *ob) */ self->flags.is_zombie = 1; - while (self->f.frame != NULL) { + for (i=0; i<10 && self->f.frame != NULL; i++) { PyTasklet_Kill(self); if (!is_mine) return; /* will be killed elsewhere */ From 580c9f7d54b4099e0401b8654b6e639126c8657b Mon Sep 17 00:00:00 2001 From: Anselm Kruis Date: Thu, 27 Dec 2018 23:39:11 +0100 Subject: [PATCH 3/4] Stackless issue #192: append non-killable tasklets to gc.garbage Move the handling of tasklets with non-trivial C-state from tasklet_traverse to tasklet_finalize. If a tasklet still has a non-trivial C-state after killing the tasklet, the finalizer appends the tasklet to gc.garbage. Now de-allocation caused by ref-counting behaves identically to de-allocation caused by GC. The gc-patch introduced by https://github.com/stackless-dev/stackless_historic/commit/35ba34f18 is now obsolete. --- Doc/library/stackless/tasklets.rst | 13 +++++ Stackless/changelog.txt | 10 +++- Stackless/module/taskletobject.c | 87 ++++++++++++++--------------- Stackless/unittests/support.py | 28 ++++++++++ Stackless/unittests/test_miscell.py | 27 ++++++++- Stackless/unittests/test_thread.py | 1 + 6 files changed, 116 insertions(+), 50 deletions(-) diff --git a/Doc/library/stackless/tasklets.rst b/Doc/library/stackless/tasklets.rst index 1f0cfacd7d4592a..202234b686e9dee 100644 --- a/Doc/library/stackless/tasklets.rst +++ b/Doc/library/stackless/tasklets.rst @@ -342,6 +342,19 @@ The ``tasklet`` class # Implement unsafe logic here. t.set_ignore_nesting(old_value) +.. method:: tasklet.__del__() + + .. versionadded:: 3.7 + + Finalize the tasklet. This is a :pep:`442` finalizer. If this tasklet is + alive and has a non-trivial C-state attached, the finalizer repeatedly + kills the tasklet for upmost 10 times until it is dead. Then, if this + tasklet still has non-trivial C-state attached, the finalizer appends the + tasklet to :data:`gc.garbage`. This is done, because releasing the C-state + could cause undefined behavior. + + You should not call this method from |PY|-code. + The following (read-only) attributes allow tasklet state to be checked: .. attribute:: tasklet.alive diff --git a/Stackless/changelog.txt b/Stackless/changelog.txt index 2e0ecf9c013b35d..f8f05b7d3554045 100644 --- a/Stackless/changelog.txt +++ b/Stackless/changelog.txt @@ -11,9 +11,13 @@ What's New in Stackless 3.X.X? - https://github.com/stackless-dev/stackless/issues/192 Use the PEP 442 mechanic to finalize a tasklet instead of a arcane logic in - tp_dealloc. The new code calls the finalizer only once. The finalizer tries - to kill the tasklet, as long as it has an unfinished C-state but at most 10- - times. This change brings the behavior more in line with generators. + tp_dealloc. The finalizer can be called from Python-code: tasklet.__del__(). + Stackless calls the finalizer only once. The finalizer repeatedly kills the + tasklet until it is dead but at most 10-times. + If the finalizer can't kill the C-state it adds the tasklet to gc.garbage. + This change unifies the behavior of de-allocation caused by ref-counting and + de-allocation caused by GC. And it brings the behavior more in line with + generators. - https://github.com/stackless-dev/stackless/issues/193 Fix the macros PyTasklet_CheckExact(op) and PyChannel_CheckExact(op). diff --git a/Stackless/module/taskletobject.c b/Stackless/module/taskletobject.c index a52bacde7ac3bef..66718e3e3ca7ace 100644 --- a/Stackless/module/taskletobject.c +++ b/Stackless/module/taskletobject.c @@ -152,12 +152,6 @@ tasklet_has_c_stack_and_thread(PyTaskletObject *t) static int tasklet_traverse(PyTaskletObject *t, visitproc visit, void *arg) { - /* tasklets that need to be switched to for the kill, can't be collected. - * Only trivial decrefs are allowed during GC collect - */ - if (tasklet_has_c_stack_and_thread(t)) - PyObject_GC_Collectable((PyObject *)t, visit, arg, 0); - Py_VISIT(t->f.frame); Py_VISIT(t->tempval); Py_VISIT(t->cstate); @@ -174,20 +168,34 @@ tasklet_clear_frames(PyTaskletObject *t) Py_CLEAR(t->f.frame); } +static inline void +exc_state_clear(_PyErr_StackItem *exc_state) +{ + PyObject *t, *v, *tb; + t = exc_state->exc_type; + v = exc_state->exc_value; + tb = exc_state->exc_traceback; + exc_state->exc_type = NULL; + exc_state->exc_value = NULL; + exc_state->exc_traceback = NULL; + Py_XDECREF(t); + Py_XDECREF(v); + Py_XDECREF(tb); +} + static void tasklet_clear(PyTaskletObject *t) { tasklet_clear_frames(t); - TASKLET_SETVAL(t, Py_None); /* always non-zero */ + Py_CLEAR(t->tempval); + Py_CLEAR(t->def_globals); /* unlink task from cstate */ if (t->cstate != NULL && t->cstate->task == t) t->cstate->task = NULL; Py_CLEAR(t->cstate); - Py_CLEAR(t->exc_state.exc_type); - Py_CLEAR(t->exc_state.exc_value); - Py_CLEAR(t->exc_state.exc_traceback); + exc_state_clear(&t->exc_state); /* Assert that the tasklet is at the end of the chain. */ assert(t->exc_state.previous_item == NULL); @@ -234,21 +242,6 @@ kill_finally (PyObject *ob) /* destructing a tasklet without destroying it */ -static inline void -exc_state_clear(_PyErr_StackItem *exc_state) -{ - PyObject *t, *v, *tb; - t = exc_state->exc_type; - v = exc_state->exc_value; - tb = exc_state->exc_traceback; - exc_state->exc_type = NULL; - exc_state->exc_value = NULL; - exc_state->exc_traceback = NULL; - Py_XDECREF(t); - Py_XDECREF(v); - Py_XDECREF(tb); -} - static void tasklet_finalize(PyObject *self) { @@ -269,6 +262,25 @@ tasklet_finalize(PyObject *self) kill_finally(self); } + /* We must not free a C-stack, that is still somewhat alive. Instead we + * add the current tasklet to gc.garbage. That's perfectly OK, because the + * tasklet is still intact. Of course this grows a new reference to the + * tasklet. + */ + if (t->f.frame && t->cstate && t->cstate->task == t && Py_SIZE(t->cstate) != 0) { + if (Py_VerboseFlag) { + PySys_WriteStderr("# tasklet_finalize: warning: tasklet %p has a non zero C-stack.\n", (void*)t); + } + if (_PyRuntime.gc.garbage == NULL) { + _PyRuntime.gc.garbage = PyList_New(0); + if (_PyRuntime.gc.garbage == NULL) + Py_FatalError("gc couldn't create gc.garbage list"); + } + TASKLET_SETVAL(t, Py_None); /* don't keep tempval alive */ + if (PyList_Append(_PyRuntime.gc.garbage, self) < 0) + PyErr_WriteUnraisable(self); + } + /* Restore the saved exception. */ PyErr_Restore(error_type, error_value, error_traceback); } @@ -287,29 +299,12 @@ tasklet_dealloc(PyTaskletObject *t) } PyObject_GC_UnTrack(t); - tasklet_clear_frames(t); + if (t->tsk_weakreflist != NULL) PyObject_ClearWeakRefs((PyObject *)t); - if (t->cstate != NULL) { - assert(t->cstate->task != t || Py_SIZE(t->cstate) == 0 || t->cstate->tstate == NULL); - if (t->cstate->task == t) { - t->cstate->task = NULL; - if (Py_VerboseFlag && Py_SIZE(t->cstate) != 0) { - PySys_WriteStderr("# tasklet_dealloc: warning: tasklet %p has a non zero C-stack. \n", (void*)t); - } - } - Py_DECREF(t->cstate); - } - Py_DECREF(t->tempval); - Py_XDECREF(t->def_globals); - /* Assert that the tasklet is at the end of the chain. */ - assert(t->exc_state.previous_item == NULL); - /* Unlink the exc_info chain. There is no guarantee, that - * the object t->exc_info points to still exists, because - * the order of calls to tp_clear is undefined. - */ - t->exc_info = &t->exc_state; - exc_state_clear(&t->exc_state); + + tasklet_clear(t); + Py_TYPE(t)->tp_free((PyObject*)t); } diff --git a/Stackless/unittests/support.py b/Stackless/unittests/support.py index 18a3ae5e13abb3c..2bd6e7f64e2f3e7 100644 --- a/Stackless/unittests/support.py +++ b/Stackless/unittests/support.py @@ -50,6 +50,16 @@ withThreads = False +def is_zombie(tasklet): + """Returns True, if tasklet is a zombie.""" + if not isinstance(tasklet, stackless.tasklet): + raise TypeError("not a tasklet") + if tasklet is stackless.current: + return False + flags = tasklet.__reduce__()[2][0] + return (flags & (1 << 6)) != 0 + + @contextlib.contextmanager def captured_stderr(): old = sys.stderr @@ -522,6 +532,7 @@ def prepare_test_methods(cls): __setup_called = False __preexisting_threads = None __active_test_cases = weakref.WeakValueDictionary() + __expected_garbage = [] def __init__(self, methodName='runTest'): # set the __enable_softswitch flag based on methodName @@ -549,6 +560,10 @@ def setUpStacklessTestCase(self): self.addCleanup(stackless.enable_softswitch, stackless.enable_softswitch(self.__enable_softswitch)) self.__active_test_cases[id(self)] = self + self.__uncollectable_tasklets = [] + self.assertListEqual([t for t in gc.garbage if isinstance(t, stackless.tasklet)], [], + "Leakage from other tests, with tasklets in gc.garbage") + watchdog_list = get_watchdog_list(-1) if watchdog_list is not None: self.assertListEqual(watchdog_list, [None], "Watchdog list is not empty: " + repr(watchdog_list)) @@ -634,6 +649,19 @@ def tearDown(self): "Leakage from other threads, with %d threads running (%d expected)" % (active_count, expected_thread_count)) gc.collect() # emits warnings about uncollectable objects after each test + unexpected_uncollectable = [] + for t in [t for t in gc.garbage if isinstance(t, stackless.tasklet)]: + if id(t) not in self.__uncollectable_tasklets: + unexpected_uncollectable.append(t) + # clean up + gc.garbage.remove(t) + self.__expected_garbage.append(t) + + self.assertListEqual(unexpected_uncollectable, [], "New uncollectable tasklets: %r" % unexpected_uncollectable) + + def tasklet_is_uncollectable(self, tlet): + self.assertIsInstance(tlet, stackless.tasklet) + self.__uncollectable_tasklets.append(id(tlet)) def dumps(self, obj, protocol=None, *, fix_imports=True): if protocol is None: diff --git a/Stackless/unittests/test_miscell.py b/Stackless/unittests/test_miscell.py index 2b13ac559aa7ab4..d697ef7cd7ad470 100644 --- a/Stackless/unittests/test_miscell.py +++ b/Stackless/unittests/test_miscell.py @@ -11,6 +11,7 @@ import time import os import struct +import gc from stackless import _test_nostacklesscall as apply_not_stackless import _teststackless @@ -22,7 +23,8 @@ withThreads = False from support import test_main # @UnusedImport -from support import StacklessTestCase, AsTaskletTestCase, require_one_thread, testcase_leaks_references +from support import (StacklessTestCase, AsTaskletTestCase, require_one_thread, + testcase_leaks_references, is_zombie) def is_soft(): @@ -1160,6 +1162,7 @@ def other_thread_main(): self.assertFalse(tlet.restorable) self.assertGreater(tlet.nesting_level, 0) self.assertRaisesRegex(RuntimeError, "tasklet has C state on its stack", tlet.bind, None) + self.tasklet_is_uncollectable(tlet) # mark this tasklet as uncollectable def test_setup_fail_alive(self): # make sure, that you can't bind a tasklet, which is alive @@ -1305,6 +1308,28 @@ def test_chain(self): c = c.next +class TestTaskletFinalizer(StacklessTestCase): + def test_zombie(self): + loop = True + + def task(): + while loop: + try: + stackless.schedule_remove() + except TaskletExit: + pass + t = stackless.tasklet(apply_not_stackless)(task,) + t.run() + self.assertTrue(t.paused) + t.__del__() + self.assertTrue(is_zombie(t)) + self.assertIn(t, gc.garbage) + gc.garbage.remove(t) + # clean up + loop = False + t.kill() + + #/////////////////////////////////////////////////////////////////////////////// if __name__ == '__main__': diff --git a/Stackless/unittests/test_thread.py b/Stackless/unittests/test_thread.py index 32c9ad527d9f2e2..85db34fa83260e5 100644 --- a/Stackless/unittests/test_thread.py +++ b/Stackless/unittests/test_thread.py @@ -440,6 +440,7 @@ def other_thread_main(): raise # the tasklet has no thread self.assertEqual(tlet.thread_id, -1) + self.tasklet_is_uncollectable(tlet) def test_methods_on_dead(self): """test that tasklet methods on a dead tasklet behave well""" From 1786ba4b38ea84e43b025c3ae20597908c0bc0cb Mon Sep 17 00:00:00 2001 From: Anselm Kruis Date: Thu, 27 Dec 2018 23:50:43 +0100 Subject: [PATCH 4/4] Stackless issue #192: revert the PyObject_GC_Collectable patch Revert the patch, that added the function PyObject_GC_Collectable(). It was introduced by https://github.com/stackless-dev/stackless_historic/commit/35ba34f18. See bpo-9141 for a discussion of this interesting patch. --- Include/objimpl.h | 2 -- Modules/gcmodule.c | 39 --------------------------------------- 2 files changed, 41 deletions(-) diff --git a/Include/objimpl.h b/Include/objimpl.h index 1f39d30d843a011..ed9e7a9680668dd 100644 --- a/Include/objimpl.h +++ b/Include/objimpl.h @@ -336,8 +336,6 @@ PyAPI_FUNC(PyVarObject *) _PyObject_GC_NewVar(PyTypeObject *, Py_ssize_t); PyAPI_FUNC(void) PyObject_GC_Track(void *); PyAPI_FUNC(void) PyObject_GC_UnTrack(void *); PyAPI_FUNC(void) PyObject_GC_Del(void *); -/* STACKLESS addition */ -PyAPI_FUNC(void) PyObject_GC_Collectable(PyObject *, visitproc, void*, int); #define PyObject_GC_New(type, typeobj) \ ( (type *) _PyObject_GC_New(typeobj) ) diff --git a/Modules/gcmodule.c b/Modules/gcmodule.c index f6d4528fd24dd26..d0d888b12d659dc 100644 --- a/Modules/gcmodule.c +++ b/Modules/gcmodule.c @@ -419,48 +419,9 @@ untrack_dicts(PyGC_Head *head) } /* Return true if object has a pre-PEP 442 finalization method. */ -/* STACKLESS addition to support collection of tasklets */ - -/* A traversal callback for has_finalisers. It is a dummy, used to identify - * this phase of finalizer discovery by its function pointer, and thus - * enable the action of PyObject_GC_Collectable() below. - */ -static int -visit_has_finalizer(PyObject *op, void *data) -{ - assert(op != NULL); - return 0; -} - -/* An object can call this function from its traversal function - * to indicate whether it wishes to be collected or not - * this is useful for objects that have state that determines - * whether non-trivial actions need to be undertaken when - * it is deleted. - */ -PyAPI_FUNC(void) -PyObject_GC_Collectable(PyObject *op, visitproc proc, void *arg, - int can_collect) -{ - /* only do this during the move_finalizers phase */ - if (proc == &visit_has_finalizer) - *(int*)arg = can_collect; -} - static int has_legacy_finalizer(PyObject *op) { - /* first, dynamic decision per object */ - traverseproc traverse; - int collectable; - traverse = Py_TYPE(op)->tp_traverse; - collectable = -1; - (void) traverse(op, - (visitproc)visit_has_finalizer, - (void *)&collectable); - if (collectable >= 0) - return collectable == 0; - return op->ob_type->tp_del != NULL; }