Skip to content

Commit 434736a

Browse files
committed
Issue #3001: Add a C implementation of recursive locks which is used by
default when instantiating a `Threading.RLock` object. This makes recursive locks as fast as regular non-recursive locks (previously, they were slower by 10x to 15x).
1 parent 0e31201 commit 434736a

File tree

4 files changed

+296
-5
lines changed

4 files changed

+296
-5
lines changed

Lib/test/test_threading.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -506,8 +506,11 @@ def test_daemonize_active_thread(self):
506506
class LockTests(lock_tests.LockTests):
507507
locktype = staticmethod(threading.Lock)
508508

509-
class RLockTests(lock_tests.RLockTests):
510-
locktype = staticmethod(threading.RLock)
509+
class PyRLockTests(lock_tests.RLockTests):
510+
locktype = staticmethod(threading._PyRLock)
511+
512+
class CRLockTests(lock_tests.RLockTests):
513+
locktype = staticmethod(threading._CRLock)
511514

512515
class EventTests(lock_tests.EventTests):
513516
eventtype = staticmethod(threading.Event)
@@ -527,7 +530,7 @@ class BoundedSemaphoreTests(lock_tests.BoundedSemaphoreTests):
527530

528531

529532
def test_main():
530-
test.support.run_unittest(LockTests, RLockTests, EventTests,
533+
test.support.run_unittest(LockTests, PyRLockTests, CRLockTests, EventTests,
531534
ConditionAsRLockTests, ConditionTests,
532535
SemaphoreTests, BoundedSemaphoreTests,
533536
ThreadTests,

Lib/threading.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@
2727
_allocate_lock = _thread.allocate_lock
2828
_get_ident = _thread.get_ident
2929
ThreadError = _thread.error
30+
try:
31+
_CRLock = _thread.RLock
32+
except AttributeError:
33+
_CRLock = None
3034
del _thread
3135

3236

@@ -79,8 +83,12 @@ def settrace(func):
7983

8084
Lock = _allocate_lock
8185

82-
def RLock(*args, **kwargs):
83-
return _RLock(*args, **kwargs)
86+
def RLock(verbose=None, *args, **kwargs):
87+
if verbose is None:
88+
verbose = _VERBOSE
89+
if (__debug__ and verbose) or _CRLock is None:
90+
return _PyRLock(verbose, *args, **kwargs)
91+
return _CRLock(*args, **kwargs)
8492

8593
class _RLock(_Verbose):
8694

@@ -156,6 +164,8 @@ def _release_save(self):
156164
def _is_owned(self):
157165
return self._owner == _get_ident()
158166

167+
_PyRLock = _RLock
168+
159169

160170
def Condition(*args, **kwargs):
161171
return _Condition(*args, **kwargs)

Misc/NEWS

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ C-API
123123
Library
124124
-------
125125

126+
- Issue #3001: Add a C implementation of recursive locks which is used by
127+
default when instantiating a `threading.RLock` object. This makes
128+
recursive locks as fast as regular non-recursive locks (previously,
129+
they were slower by 10x to 15x).
130+
126131
- Issue #7282: Fix a memory leak when an RLock was used in a thread other
127132
than those started through `threading.Thread` (for example, using
128133
`_thread.start_new_thread()`).

Modules/_threadmodule.c

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,273 @@ static PyTypeObject Locktype = {
155155
lock_methods, /*tp_methods*/
156156
};
157157

158+
/* Recursive lock objects */
159+
160+
typedef struct {
161+
PyObject_HEAD
162+
PyThread_type_lock rlock_lock;
163+
long rlock_owner;
164+
unsigned long rlock_count;
165+
PyObject *in_weakreflist;
166+
} rlockobject;
167+
168+
static void
169+
rlock_dealloc(rlockobject *self)
170+
{
171+
assert(self->rlock_lock);
172+
if (self->in_weakreflist != NULL)
173+
PyObject_ClearWeakRefs((PyObject *) self);
174+
/* Unlock the lock so it's safe to free it */
175+
if (self->rlock_count > 0)
176+
PyThread_release_lock(self->rlock_lock);
177+
178+
PyThread_free_lock(self->rlock_lock);
179+
Py_TYPE(self)->tp_free(self);
180+
}
181+
182+
static PyObject *
183+
rlock_acquire(rlockobject *self, PyObject *args, PyObject *kwds)
184+
{
185+
char *kwlist[] = {"blocking", NULL};
186+
int blocking = 1;
187+
long tid;
188+
int r = 1;
189+
190+
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|i:acquire", kwlist,
191+
&blocking))
192+
return NULL;
193+
194+
tid = PyThread_get_thread_ident();
195+
if (self->rlock_count > 0 && tid == self->rlock_owner) {
196+
unsigned long count = self->rlock_count + 1;
197+
if (count <= self->rlock_count) {
198+
PyErr_SetString(PyExc_OverflowError,
199+
"Internal lock count overflowed");
200+
return NULL;
201+
}
202+
self->rlock_count = count;
203+
Py_RETURN_TRUE;
204+
}
205+
206+
if (self->rlock_count > 0 ||
207+
!PyThread_acquire_lock(self->rlock_lock, 0)) {
208+
if (!blocking) {
209+
Py_RETURN_FALSE;
210+
}
211+
Py_BEGIN_ALLOW_THREADS
212+
r = PyThread_acquire_lock(self->rlock_lock, blocking);
213+
Py_END_ALLOW_THREADS
214+
}
215+
if (r) {
216+
assert(self->rlock_count == 0);
217+
self->rlock_owner = tid;
218+
self->rlock_count = 1;
219+
}
220+
221+
return PyBool_FromLong(r);
222+
}
223+
224+
PyDoc_STRVAR(rlock_acquire_doc,
225+
"acquire(blocking=True) -> bool\n\
226+
\n\
227+
Lock the lock. `blocking` indicates whether we should wait\n\
228+
for the lock to be available or not. If `blocking` is False\n\
229+
and another thread holds the lock, the method will return False\n\
230+
immediately. If `blocking` is True and another thread holds\n\
231+
the lock, the method will wait for the lock to be released,\n\
232+
take it and then return True.\n\
233+
(note: the blocking operation is not interruptible.)\n\
234+
\n\
235+
In all other cases, the method will return True immediately.\n\
236+
Precisely, if the current thread already holds the lock, its\n\
237+
internal counter is simply incremented. If nobody holds the lock,\n\
238+
the lock is taken and its internal counter initialized to 1.");
239+
240+
static PyObject *
241+
rlock_release(rlockobject *self)
242+
{
243+
long tid = PyThread_get_thread_ident();
244+
245+
if (self->rlock_count == 0 || self->rlock_owner != tid) {
246+
PyErr_SetString(PyExc_RuntimeError,
247+
"cannot release un-acquired lock");
248+
return NULL;
249+
}
250+
if (--self->rlock_count == 0) {
251+
self->rlock_owner = 0;
252+
PyThread_release_lock(self->rlock_lock);
253+
}
254+
Py_RETURN_NONE;
255+
}
256+
257+
PyDoc_STRVAR(rlock_release_doc,
258+
"release()\n\
259+
\n\
260+
Release the lock, allowing another thread that is blocked waiting for\n\
261+
the lock to acquire the lock. The lock must be in the locked state,\n\
262+
and must be locked by the same thread that unlocks it; otherwise a\n\
263+
`RuntimeError` is raised.\n\
264+
\n\
265+
Do note that if the lock was acquire()d several times in a row by the\n\
266+
current thread, release() needs to be called as many times for the lock\n\
267+
to be available for other threads.");
268+
269+
static PyObject *
270+
rlock_acquire_restore(rlockobject *self, PyObject *arg)
271+
{
272+
long owner;
273+
unsigned long count;
274+
int r = 1;
275+
276+
if (!PyArg_ParseTuple(arg, "kl:_acquire_restore", &count, &owner))
277+
return NULL;
278+
279+
if (!PyThread_acquire_lock(self->rlock_lock, 0)) {
280+
Py_BEGIN_ALLOW_THREADS
281+
r = PyThread_acquire_lock(self->rlock_lock, 1);
282+
Py_END_ALLOW_THREADS
283+
}
284+
if (!r) {
285+
PyErr_SetString(ThreadError, "couldn't acquire lock");
286+
return NULL;
287+
}
288+
assert(self->rlock_count == 0);
289+
self->rlock_owner = owner;
290+
self->rlock_count = count;
291+
Py_RETURN_NONE;
292+
}
293+
294+
PyDoc_STRVAR(rlock_acquire_restore_doc,
295+
"_acquire_restore(state) -> None\n\
296+
\n\
297+
For internal use by `threading.Condition`.");
298+
299+
static PyObject *
300+
rlock_release_save(rlockobject *self)
301+
{
302+
long owner;
303+
unsigned long count;
304+
305+
owner = self->rlock_owner;
306+
count = self->rlock_count;
307+
self->rlock_count = 0;
308+
self->rlock_owner = 0;
309+
PyThread_release_lock(self->rlock_lock);
310+
return Py_BuildValue("kl", count, owner);
311+
}
312+
313+
PyDoc_STRVAR(rlock_release_save_doc,
314+
"_release_save() -> tuple\n\
315+
\n\
316+
For internal use by `threading.Condition`.");
317+
318+
319+
static PyObject *
320+
rlock_is_owned(rlockobject *self)
321+
{
322+
long tid = PyThread_get_thread_ident();
323+
324+
if (self->rlock_count > 0 && self->rlock_owner == tid) {
325+
Py_RETURN_TRUE;
326+
}
327+
Py_RETURN_FALSE;
328+
}
329+
330+
PyDoc_STRVAR(rlock_is_owned_doc,
331+
"_is_owned() -> bool\n\
332+
\n\
333+
For internal use by `threading.Condition`.");
334+
335+
static PyObject *
336+
rlock_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
337+
{
338+
rlockobject *self;
339+
340+
self = (rlockobject *) type->tp_alloc(type, 0);
341+
if (self != NULL) {
342+
self->rlock_lock = PyThread_allocate_lock();
343+
if (self->rlock_lock == NULL) {
344+
type->tp_free(self);
345+
PyErr_SetString(ThreadError, "can't allocate lock");
346+
return NULL;
347+
}
348+
self->in_weakreflist = NULL;
349+
self->rlock_owner = 0;
350+
self->rlock_count = 0;
351+
}
352+
353+
return (PyObject *) self;
354+
}
355+
356+
static PyObject *
357+
rlock_repr(rlockobject *self)
358+
{
359+
return PyUnicode_FromFormat("<%s owner=%ld count=%lu>",
360+
Py_TYPE(self)->tp_name, self->rlock_owner, self->rlock_count);
361+
}
362+
363+
364+
static PyMethodDef rlock_methods[] = {
365+
{"acquire", (PyCFunction)rlock_acquire,
366+
METH_VARARGS | METH_KEYWORDS, rlock_acquire_doc},
367+
{"release", (PyCFunction)rlock_release,
368+
METH_NOARGS, rlock_release_doc},
369+
{"_is_owned", (PyCFunction)rlock_is_owned,
370+
METH_NOARGS, rlock_is_owned_doc},
371+
{"_acquire_restore", (PyCFunction)rlock_acquire_restore,
372+
METH_O, rlock_acquire_restore_doc},
373+
{"_release_save", (PyCFunction)rlock_release_save,
374+
METH_NOARGS, rlock_release_save_doc},
375+
{"__enter__", (PyCFunction)rlock_acquire,
376+
METH_VARARGS | METH_KEYWORDS, rlock_acquire_doc},
377+
{"__exit__", (PyCFunction)rlock_release,
378+
METH_VARARGS, rlock_release_doc},
379+
{NULL, NULL} /* sentinel */
380+
};
381+
382+
383+
static PyTypeObject RLocktype = {
384+
PyVarObject_HEAD_INIT(&PyType_Type, 0)
385+
"_thread.RLock", /*tp_name*/
386+
sizeof(rlockobject), /*tp_size*/
387+
0, /*tp_itemsize*/
388+
/* methods */
389+
(destructor)rlock_dealloc, /*tp_dealloc*/
390+
0, /*tp_print*/
391+
0, /*tp_getattr*/
392+
0, /*tp_setattr*/
393+
0, /*tp_reserved*/
394+
(reprfunc)rlock_repr, /*tp_repr*/
395+
0, /*tp_as_number*/
396+
0, /*tp_as_sequence*/
397+
0, /*tp_as_mapping*/
398+
0, /*tp_hash*/
399+
0, /*tp_call*/
400+
0, /*tp_str*/
401+
0, /*tp_getattro*/
402+
0, /*tp_setattro*/
403+
0, /*tp_as_buffer*/
404+
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */
405+
0, /*tp_doc*/
406+
0, /*tp_traverse*/
407+
0, /*tp_clear*/
408+
0, /*tp_richcompare*/
409+
offsetof(rlockobject, in_weakreflist), /*tp_weaklistoffset*/
410+
0, /*tp_iter*/
411+
0, /*tp_iternext*/
412+
rlock_methods, /*tp_methods*/
413+
0, /* tp_members */
414+
0, /* tp_getset */
415+
0, /* tp_base */
416+
0, /* tp_dict */
417+
0, /* tp_descr_get */
418+
0, /* tp_descr_set */
419+
0, /* tp_dictoffset */
420+
0, /* tp_init */
421+
PyType_GenericAlloc, /* tp_alloc */
422+
rlock_new /* tp_new */
423+
};
424+
158425
static lockobject *
159426
newlockobject(void)
160427
{
@@ -752,6 +1019,8 @@ PyInit__thread(void)
7521019
return NULL;
7531020
if (PyType_Ready(&Locktype) < 0)
7541021
return NULL;
1022+
if (PyType_Ready(&RLocktype) < 0)
1023+
return NULL;
7551024

7561025
/* Create the module and add the functions */
7571026
m = PyModule_Create(&threadmodule);
@@ -766,6 +1035,10 @@ PyInit__thread(void)
7661035
Py_INCREF(&Locktype);
7671036
PyDict_SetItemString(d, "LockType", (PyObject *)&Locktype);
7681037

1038+
Py_INCREF(&RLocktype);
1039+
if (PyModule_AddObject(m, "RLock", (PyObject *)&RLocktype) < 0)
1040+
return NULL;
1041+
7691042
Py_INCREF(&localtype);
7701043
if (PyModule_AddObject(m, "_local", (PyObject *)&localtype) < 0)
7711044
return NULL;

0 commit comments

Comments
 (0)