From 72a0d218dcc94a3cc409a9ef32dfcd5a7bbcb43c Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Mon, 29 Jan 2018 20:37:09 +0000 Subject: bpo-31356: Add context manager to temporarily disable GC (GH-4224) --- Doc/library/gc.rst | 28 ++++++ Include/internal/mem.h | 1 + Lib/test/test_gc.py | 71 +++++++++++++- .../2017-11-02-00-34-42.bpo-31356.54Lb8U.rst | 3 + Modules/gcmodule.c | 106 +++++++++++++++++++++ 5 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2017-11-02-00-34-42.bpo-31356.54Lb8U.rst diff --git a/Doc/library/gc.rst b/Doc/library/gc.rst index 153d8fb..92240c7 100644 --- a/Doc/library/gc.rst +++ b/Doc/library/gc.rst @@ -33,6 +33,34 @@ The :mod:`gc` module provides the following functions: Disable automatic garbage collection. +.. class:: ensure_disabled() + + Return a context manager object that disables the garbage collector and reenables the previous + state upon completion of the block. This is basically equivalent to:: + + from gc import enable, disable, isenabled + + @contextmanager + def ensure_disabled(): + was_enabled_previously = isenabled() + gc.disable() + yield + if was_enabled_previously: + gc.enable() + + And lets you write code like this:: + + with ensure_disabled(): + run_some_timing() + + with ensure_disabled(): + # do_something_that_has_real_time_guarantees + # such as a pair trade, robotic braking, etc + + without needing to explicitly enable and disable the garbage collector yourself. + This context manager is implemented in C to assure atomicity, thread safety and speed. + + .. function:: isenabled() Returns true if automatic collection is enabled. diff --git a/Include/internal/mem.h b/Include/internal/mem.h index a731e30..4a84b4a 100644 --- a/Include/internal/mem.h +++ b/Include/internal/mem.h @@ -116,6 +116,7 @@ struct _gc_runtime_state { int enabled; int debug; + long disabled_threads; /* linked lists of container objects */ struct gc_generation generations[NUM_GENERATIONS]; PyGC_Head *generation0; diff --git a/Lib/test/test_gc.py b/Lib/test/test_gc.py index 904fc7d..246980a 100644 --- a/Lib/test/test_gc.py +++ b/Lib/test/test_gc.py @@ -1,7 +1,7 @@ import unittest from test.support import (verbose, refcount_test, run_unittest, strip_python_stderr, cpython_only, start_threads, - temp_dir, requires_type_collecting) + temp_dir, requires_type_collecting,reap_threads) from test.support.script_helper import assert_python_ok, make_script import sys @@ -9,6 +9,8 @@ import time import gc import weakref import threading +import warnings + try: from _testcapi import with_tp_del @@ -1007,6 +1009,73 @@ class GCTogglingTests(unittest.TestCase): # empty __dict__. self.assertEqual(x, None) + def test_ensure_disabled(self): + original_status = gc.isenabled() + + with gc.ensure_disabled(): + inside_status = gc.isenabled() + + after_status = gc.isenabled() + self.assertEqual(original_status, True) + self.assertEqual(inside_status, False) + self.assertEqual(after_status, True) + + def test_ensure_disabled_with_gc_disabled(self): + gc.disable() + + original_status = gc.isenabled() + + with gc.ensure_disabled(): + inside_status = gc.isenabled() + + after_status = gc.isenabled() + self.assertEqual(original_status, False) + self.assertEqual(inside_status, False) + self.assertEqual(after_status, False) + + @reap_threads + def test_ensure_disabled_thread(self): + + thread_original_status = None + thread_inside_status = None + thread_after_status = None + + def disabling_thread(): + nonlocal thread_original_status + nonlocal thread_inside_status + nonlocal thread_after_status + thread_original_status = gc.isenabled() + + with gc.ensure_disabled(): + time.sleep(0.01) + thread_inside_status = gc.isenabled() + + thread_after_status = gc.isenabled() + + original_status = gc.isenabled() + + with warnings.catch_warnings(record=True) as w, gc.ensure_disabled(): + inside_status_before_thread = gc.isenabled() + thread = threading.Thread(target=disabling_thread) + thread.start() + inside_status_after_thread = gc.isenabled() + + after_status = gc.isenabled() + thread.join() + + self.assertEqual(len(w), 1) + self.assertTrue(issubclass(w[-1].category, RuntimeWarning)) + self.assertEqual("Garbage collector enabled while another thread is " + "inside gc.ensure_enabled", str(w[-1].message)) + self.assertEqual(original_status, True) + self.assertEqual(inside_status_before_thread, False) + self.assertEqual(thread_original_status, False) + self.assertEqual(thread_inside_status, True) + self.assertEqual(thread_after_status, False) + self.assertEqual(inside_status_after_thread, False) + self.assertEqual(after_status, True) + + def test_main(): enabled = gc.isenabled() gc.disable() diff --git a/Misc/NEWS.d/next/Core and Builtins/2017-11-02-00-34-42.bpo-31356.54Lb8U.rst b/Misc/NEWS.d/next/Core and Builtins/2017-11-02-00-34-42.bpo-31356.54Lb8U.rst new file mode 100644 index 0000000..792f314 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2017-11-02-00-34-42.bpo-31356.54Lb8U.rst @@ -0,0 +1,3 @@ +Add a new contextmanager to the gc module that temporarily disables the GC +and restores the previous state. The implementation is done in C to assure +atomicity and speed. diff --git a/Modules/gcmodule.c b/Modules/gcmodule.c index 8ba1093..c057d25 100644 --- a/Modules/gcmodule.c +++ b/Modules/gcmodule.c @@ -1067,6 +1067,10 @@ static PyObject * gc_enable_impl(PyObject *module) /*[clinic end generated code: output=45a427e9dce9155c input=81ac4940ca579707]*/ { + if(_PyRuntime.gc.disabled_threads){ + PyErr_WarnEx(PyExc_RuntimeWarning, "Garbage collector enabled while another " + "thread is inside gc.ensure_enabled",1); + } _PyRuntime.gc.enabled = 1; Py_RETURN_NONE; } @@ -1508,6 +1512,102 @@ static PyMethodDef GcMethods[] = { {NULL, NULL} /* Sentinel */ }; +typedef struct { + PyObject_HEAD + int previous_gc_state; +} ensure_disabled_object; + + +static void +ensure_disabled_object_dealloc(ensure_disabled_object *m_obj) +{ + Py_TYPE(m_obj)->tp_free((PyObject*)m_obj); +} + +static PyObject * +ensure_disabled__enter__method(ensure_disabled_object *self, PyObject *args) +{ + PyGILState_STATE gstate = PyGILState_Ensure(); + ++_PyRuntime.gc.disabled_threads; + self->previous_gc_state = _PyRuntime.gc.enabled; + gc_disable_impl(NULL); + PyGILState_Release(gstate); + Py_RETURN_NONE; +} + +static PyObject * +ensure_disabled__exit__method(ensure_disabled_object *self, PyObject *args) +{ + PyGILState_STATE gstate = PyGILState_Ensure(); + --_PyRuntime.gc.disabled_threads; + if(self->previous_gc_state){ + gc_enable_impl(NULL); + }else{ + gc_disable_impl(NULL); + } + PyGILState_Release(gstate); + Py_RETURN_NONE; +} + + + +static struct PyMethodDef ensure_disabled_object_methods[] = { + {"__enter__", (PyCFunction) ensure_disabled__enter__method, METH_NOARGS}, + {"__exit__", (PyCFunction) ensure_disabled__exit__method, METH_VARARGS}, + {NULL, NULL} /* sentinel */ +}; + +static PyObject * +new_disabled_obj(PyTypeObject *type, PyObject *args, PyObject *kwdict){ + ensure_disabled_object *self; + self = (ensure_disabled_object *)type->tp_alloc(type, 0); + return (PyObject *) self; +}; + +static PyTypeObject gc_ensure_disabled_type = { + PyVarObject_HEAD_INIT(NULL, 0) + "gc.ensure_disabled", /* tp_name */ + sizeof(ensure_disabled_object), /* tp_size */ + 0, /* tp_itemsize */ + /* methods */ + (destructor) ensure_disabled_object_dealloc,/* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_reserved */ + 0, /* tp_repr */ + 0, /* tp_as_number */ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + PyObject_GenericGetAttr, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + ensure_disabled_object_methods, /* tp_methods */ + 0, /* tp_members */ + 0, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + 0, /* tp_init */ + PyType_GenericAlloc, /* tp_alloc */ + new_disabled_obj, /* tp_new */ + PyObject_Del, /* tp_free */ +}; + + static struct PyModuleDef gcmodule = { PyModuleDef_HEAD_INIT, "gc", /* m_name */ @@ -1548,6 +1648,12 @@ PyInit_gc(void) if (PyModule_AddObject(m, "callbacks", _PyRuntime.gc.callbacks) < 0) return NULL; + if (PyType_Ready(&gc_ensure_disabled_type) < 0) + return NULL; + if (PyModule_AddObject(m, "ensure_disabled", (PyObject*) &gc_ensure_disabled_type) < 0) + return NULL; + + #define ADD_INT(NAME) if (PyModule_AddIntConstant(m, #NAME, NAME) < 0) return NULL ADD_INT(DEBUG_STATS); ADD_INT(DEBUG_COLLECTABLE); -- cgit v0.12