Skip to content

Commit 72a0d21

Browse files
pablogsalrhettinger
authored andcommitted
bpo-31356: Add context manager to temporarily disable GC (GH-4224)
1 parent 0cd6bca commit 72a0d21

File tree

5 files changed

+208
-1
lines changed

5 files changed

+208
-1
lines changed

Doc/library/gc.rst

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,34 @@ The :mod:`gc` module provides the following functions:
3333
Disable automatic garbage collection.
3434

3535

36+
.. class:: ensure_disabled()
37+
38+
Return a context manager object that disables the garbage collector and reenables the previous
39+
state upon completion of the block. This is basically equivalent to::
40+
41+
from gc import enable, disable, isenabled
42+
43+
@contextmanager
44+
def ensure_disabled():
45+
was_enabled_previously = isenabled()
46+
gc.disable()
47+
yield
48+
if was_enabled_previously:
49+
gc.enable()
50+
51+
And lets you write code like this::
52+
53+
with ensure_disabled():
54+
run_some_timing()
55+
56+
with ensure_disabled():
57+
# do_something_that_has_real_time_guarantees
58+
# such as a pair trade, robotic braking, etc
59+
60+
without needing to explicitly enable and disable the garbage collector yourself.
61+
This context manager is implemented in C to assure atomicity, thread safety and speed.
62+
63+
3664
.. function:: isenabled()
3765

3866
Returns true if automatic collection is enabled.

Include/internal/mem.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ struct _gc_runtime_state {
116116

117117
int enabled;
118118
int debug;
119+
long disabled_threads;
119120
/* linked lists of container objects */
120121
struct gc_generation generations[NUM_GENERATIONS];
121122
PyGC_Head *generation0;

Lib/test/test_gc.py

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import unittest
22
from test.support import (verbose, refcount_test, run_unittest,
33
strip_python_stderr, cpython_only, start_threads,
4-
temp_dir, requires_type_collecting)
4+
temp_dir, requires_type_collecting,reap_threads)
55
from test.support.script_helper import assert_python_ok, make_script
66

77
import sys
88
import time
99
import gc
1010
import weakref
1111
import threading
12+
import warnings
13+
1214

1315
try:
1416
from _testcapi import with_tp_del
@@ -1007,6 +1009,73 @@ def __del__(self):
10071009
# empty __dict__.
10081010
self.assertEqual(x, None)
10091011

1012+
def test_ensure_disabled(self):
1013+
original_status = gc.isenabled()
1014+
1015+
with gc.ensure_disabled():
1016+
inside_status = gc.isenabled()
1017+
1018+
after_status = gc.isenabled()
1019+
self.assertEqual(original_status, True)
1020+
self.assertEqual(inside_status, False)
1021+
self.assertEqual(after_status, True)
1022+
1023+
def test_ensure_disabled_with_gc_disabled(self):
1024+
gc.disable()
1025+
1026+
original_status = gc.isenabled()
1027+
1028+
with gc.ensure_disabled():
1029+
inside_status = gc.isenabled()
1030+
1031+
after_status = gc.isenabled()
1032+
self.assertEqual(original_status, False)
1033+
self.assertEqual(inside_status, False)
1034+
self.assertEqual(after_status, False)
1035+
1036+
@reap_threads
1037+
def test_ensure_disabled_thread(self):
1038+
1039+
thread_original_status = None
1040+
thread_inside_status = None
1041+
thread_after_status = None
1042+
1043+
def disabling_thread():
1044+
nonlocal thread_original_status
1045+
nonlocal thread_inside_status
1046+
nonlocal thread_after_status
1047+
thread_original_status = gc.isenabled()
1048+
1049+
with gc.ensure_disabled():
1050+
time.sleep(0.01)
1051+
thread_inside_status = gc.isenabled()
1052+
1053+
thread_after_status = gc.isenabled()
1054+
1055+
original_status = gc.isenabled()
1056+
1057+
with warnings.catch_warnings(record=True) as w, gc.ensure_disabled():
1058+
inside_status_before_thread = gc.isenabled()
1059+
thread = threading.Thread(target=disabling_thread)
1060+
thread.start()
1061+
inside_status_after_thread = gc.isenabled()
1062+
1063+
after_status = gc.isenabled()
1064+
thread.join()
1065+
1066+
self.assertEqual(len(w), 1)
1067+
self.assertTrue(issubclass(w[-1].category, RuntimeWarning))
1068+
self.assertEqual("Garbage collector enabled while another thread is "
1069+
"inside gc.ensure_enabled", str(w[-1].message))
1070+
self.assertEqual(original_status, True)
1071+
self.assertEqual(inside_status_before_thread, False)
1072+
self.assertEqual(thread_original_status, False)
1073+
self.assertEqual(thread_inside_status, True)
1074+
self.assertEqual(thread_after_status, False)
1075+
self.assertEqual(inside_status_after_thread, False)
1076+
self.assertEqual(after_status, True)
1077+
1078+
10101079
def test_main():
10111080
enabled = gc.isenabled()
10121081
gc.disable()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add a new contextmanager to the gc module that temporarily disables the GC
2+
and restores the previous state. The implementation is done in C to assure
3+
atomicity and speed.

Modules/gcmodule.c

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,6 +1067,10 @@ static PyObject *
10671067
gc_enable_impl(PyObject *module)
10681068
/*[clinic end generated code: output=45a427e9dce9155c input=81ac4940ca579707]*/
10691069
{
1070+
if(_PyRuntime.gc.disabled_threads){
1071+
PyErr_WarnEx(PyExc_RuntimeWarning, "Garbage collector enabled while another "
1072+
"thread is inside gc.ensure_enabled",1);
1073+
}
10701074
_PyRuntime.gc.enabled = 1;
10711075
Py_RETURN_NONE;
10721076
}
@@ -1508,6 +1512,102 @@ static PyMethodDef GcMethods[] = {
15081512
{NULL, NULL} /* Sentinel */
15091513
};
15101514

1515+
typedef struct {
1516+
PyObject_HEAD
1517+
int previous_gc_state;
1518+
} ensure_disabled_object;
1519+
1520+
1521+
static void
1522+
ensure_disabled_object_dealloc(ensure_disabled_object *m_obj)
1523+
{
1524+
Py_TYPE(m_obj)->tp_free((PyObject*)m_obj);
1525+
}
1526+
1527+
static PyObject *
1528+
ensure_disabled__enter__method(ensure_disabled_object *self, PyObject *args)
1529+
{
1530+
PyGILState_STATE gstate = PyGILState_Ensure();
1531+
++_PyRuntime.gc.disabled_threads;
1532+
self->previous_gc_state = _PyRuntime.gc.enabled;
1533+
gc_disable_impl(NULL);
1534+
PyGILState_Release(gstate);
1535+
Py_RETURN_NONE;
1536+
}
1537+
1538+
static PyObject *
1539+
ensure_disabled__exit__method(ensure_disabled_object *self, PyObject *args)
1540+
{
1541+
PyGILState_STATE gstate = PyGILState_Ensure();
1542+
--_PyRuntime.gc.disabled_threads;
1543+
if(self->previous_gc_state){
1544+
gc_enable_impl(NULL);
1545+
}else{
1546+
gc_disable_impl(NULL);
1547+
}
1548+
PyGILState_Release(gstate);
1549+
Py_RETURN_NONE;
1550+
}
1551+
1552+
1553+
1554+
static struct PyMethodDef ensure_disabled_object_methods[] = {
1555+
{"__enter__", (PyCFunction) ensure_disabled__enter__method, METH_NOARGS},
1556+
{"__exit__", (PyCFunction) ensure_disabled__exit__method, METH_VARARGS},
1557+
{NULL, NULL} /* sentinel */
1558+
};
1559+
1560+
static PyObject *
1561+
new_disabled_obj(PyTypeObject *type, PyObject *args, PyObject *kwdict){
1562+
ensure_disabled_object *self;
1563+
self = (ensure_disabled_object *)type->tp_alloc(type, 0);
1564+
return (PyObject *) self;
1565+
};
1566+
1567+
static PyTypeObject gc_ensure_disabled_type = {
1568+
PyVarObject_HEAD_INIT(NULL, 0)
1569+
"gc.ensure_disabled", /* tp_name */
1570+
sizeof(ensure_disabled_object), /* tp_size */
1571+
0, /* tp_itemsize */
1572+
/* methods */
1573+
(destructor) ensure_disabled_object_dealloc,/* tp_dealloc */
1574+
0, /* tp_print */
1575+
0, /* tp_getattr */
1576+
0, /* tp_setattr */
1577+
0, /* tp_reserved */
1578+
0, /* tp_repr */
1579+
0, /* tp_as_number */
1580+
0, /*tp_as_sequence*/
1581+
0, /*tp_as_mapping*/
1582+
0, /*tp_hash*/
1583+
0, /*tp_call*/
1584+
0, /*tp_str*/
1585+
PyObject_GenericGetAttr, /*tp_getattro*/
1586+
0, /*tp_setattro*/
1587+
0, /*tp_as_buffer*/
1588+
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/
1589+
0, /*tp_doc*/
1590+
0, /* tp_traverse */
1591+
0, /* tp_clear */
1592+
0, /* tp_richcompare */
1593+
0, /* tp_weaklistoffset */
1594+
0, /* tp_iter */
1595+
0, /* tp_iternext */
1596+
ensure_disabled_object_methods, /* tp_methods */
1597+
0, /* tp_members */
1598+
0, /* tp_getset */
1599+
0, /* tp_base */
1600+
0, /* tp_dict */
1601+
0, /* tp_descr_get */
1602+
0, /* tp_descr_set */
1603+
0, /* tp_dictoffset */
1604+
0, /* tp_init */
1605+
PyType_GenericAlloc, /* tp_alloc */
1606+
new_disabled_obj, /* tp_new */
1607+
PyObject_Del, /* tp_free */
1608+
};
1609+
1610+
15111611
static struct PyModuleDef gcmodule = {
15121612
PyModuleDef_HEAD_INIT,
15131613
"gc", /* m_name */
@@ -1548,6 +1648,12 @@ PyInit_gc(void)
15481648
if (PyModule_AddObject(m, "callbacks", _PyRuntime.gc.callbacks) < 0)
15491649
return NULL;
15501650

1651+
if (PyType_Ready(&gc_ensure_disabled_type) < 0)
1652+
return NULL;
1653+
if (PyModule_AddObject(m, "ensure_disabled", (PyObject*) &gc_ensure_disabled_type) < 0)
1654+
return NULL;
1655+
1656+
15511657
#define ADD_INT(NAME) if (PyModule_AddIntConstant(m, #NAME, NAME) < 0) return NULL
15521658
ADD_INT(DEBUG_STATS);
15531659
ADD_INT(DEBUG_COLLECTABLE);

0 commit comments

Comments
 (0)