Skip to content

Commit 87255be

Browse files
authored
bpo-40089: Add _at_fork_reinit() method to locks (pythonGH-19195)
Add a private _at_fork_reinit() method to _thread.Lock, _thread.RLock, threading.RLock and threading.Condition classes: reinitialize the lock after fork in the child process; reset the lock to the unlocked state. Rename also the private _reset_internal_locks() method of threading.Event to _at_fork_reinit(). * Add _PyThread_at_fork_reinit() private function. It is excluded from the limited C API. * threading.Thread._reset_internal_locks() now calls _at_fork_reinit() on self._tstate_lock rather than creating a new Python lock object.
1 parent 48b069a commit 87255be

File tree

7 files changed

+133
-22
lines changed

7 files changed

+133
-22
lines changed

Include/pythread.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ PyAPI_FUNC(int) PyThread_acquire_lock(PyThread_type_lock, int);
3636
#define WAIT_LOCK 1
3737
#define NOWAIT_LOCK 0
3838

39+
#ifndef Py_LIMITED_API
40+
#ifdef HAVE_FORK
41+
/* Private function to reinitialize a lock at fork in the child process.
42+
Reset the lock to the unlocked state.
43+
Return 0 on success, return -1 on error. */
44+
PyAPI_FUNC(int) _PyThread_at_fork_reinit(PyThread_type_lock *lock);
45+
#endif /* HAVE_FORK */
46+
#endif /* !Py_LIMITED_API */
47+
3948
/* PY_TIMEOUT_T is the integral type used to specify timeouts when waiting
4049
on a lock (see PyThread_acquire_lock_timed() below).
4150
PY_TIMEOUT_MAX is the highest usable value (in microseconds) of that

Lib/test/lock_tests.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Various tests for synchronization primitives.
33
"""
44

5+
import os
56
import sys
67
import time
78
from _thread import start_new_thread, TIMEOUT_MAX
@@ -12,6 +13,11 @@
1213
from test import support
1314

1415

16+
requires_fork = unittest.skipUnless(hasattr(os, 'fork'),
17+
"platform doesn't support fork "
18+
"(no _at_fork_reinit method)")
19+
20+
1521
def _wait():
1622
# A crude wait/yield function not relying on synchronization primitives.
1723
time.sleep(0.01)
@@ -265,6 +271,25 @@ def test_state_after_timeout(self):
265271
self.assertFalse(lock.locked())
266272
self.assertTrue(lock.acquire(blocking=False))
267273

274+
@requires_fork
275+
def test_at_fork_reinit(self):
276+
def use_lock(lock):
277+
# make sure that the lock still works normally
278+
# after _at_fork_reinit()
279+
lock.acquire()
280+
lock.release()
281+
282+
# unlocked
283+
lock = self.locktype()
284+
lock._at_fork_reinit()
285+
use_lock(lock)
286+
287+
# locked: _at_fork_reinit() resets the lock to the unlocked state
288+
lock2 = self.locktype()
289+
lock2.acquire()
290+
lock2._at_fork_reinit()
291+
use_lock(lock2)
292+
268293

269294
class RLockTests(BaseLockTests):
270295
"""
@@ -417,12 +442,13 @@ def f():
417442
b.wait_for_finished()
418443
self.assertEqual(results, [True] * N)
419444

420-
def test_reset_internal_locks(self):
445+
@requires_fork
446+
def test_at_fork_reinit(self):
421447
# ensure that condition is still using a Lock after reset
422448
evt = self.eventtype()
423449
with evt._cond:
424450
self.assertFalse(evt._cond.acquire(False))
425-
evt._reset_internal_locks()
451+
evt._at_fork_reinit()
426452
with evt._cond:
427453
self.assertFalse(evt._cond.acquire(False))
428454

Lib/threading.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ def __repr__(self):
123123
hex(id(self))
124124
)
125125

126+
def _at_fork_reinit(self):
127+
self._block._at_fork_reinit()
128+
self._owner = None
129+
self._count = 0
130+
126131
def acquire(self, blocking=True, timeout=-1):
127132
"""Acquire a lock, blocking or non-blocking.
128133
@@ -245,6 +250,10 @@ def __init__(self, lock=None):
245250
pass
246251
self._waiters = _deque()
247252

253+
def _at_fork_reinit(self):
254+
self._lock._at_fork_reinit()
255+
self._waiters.clear()
256+
248257
def __enter__(self):
249258
return self._lock.__enter__()
250259

@@ -514,9 +523,9 @@ def __init__(self):
514523
self._cond = Condition(Lock())
515524
self._flag = False
516525

517-
def _reset_internal_locks(self):
518-
# private! called by Thread._reset_internal_locks by _after_fork()
519-
self._cond.__init__(Lock())
526+
def _at_fork_reinit(self):
527+
# Private method called by Thread._reset_internal_locks()
528+
self._cond._at_fork_reinit()
520529

521530
def is_set(self):
522531
"""Return true if and only if the internal flag is true."""
@@ -816,9 +825,10 @@ class is implemented.
816825
def _reset_internal_locks(self, is_alive):
817826
# private! Called by _after_fork() to reset our internal locks as
818827
# they may be in an invalid state leading to a deadlock or crash.
819-
self._started._reset_internal_locks()
828+
self._started._at_fork_reinit()
820829
if is_alive:
821-
self._set_tstate_lock()
830+
self._tstate_lock._at_fork_reinit()
831+
self._tstate_lock.acquire()
822832
else:
823833
# The thread isn't alive after fork: it doesn't have a tstate
824834
# anymore.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Add a private ``_at_fork_reinit()`` method to :class:`_thread.Lock`,
2+
:class:`_thread.RLock`, :class:`threading.RLock` and
3+
:class:`threading.Condition` classes: reinitialize the lock at fork in the
4+
child process, reset the lock to the unlocked state.
5+
Rename also the private ``_reset_internal_locks()`` method of
6+
:class:`threading.Event` to ``_at_fork_reinit()``.

Modules/_threadmodule.c

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,22 @@ lock_repr(lockobject *self)
213213
self->locked ? "locked" : "unlocked", Py_TYPE(self)->tp_name, self);
214214
}
215215

216+
#ifdef HAVE_FORK
217+
static PyObject *
218+
lock__at_fork_reinit(lockobject *self, PyObject *Py_UNUSED(args))
219+
{
220+
if (_PyThread_at_fork_reinit(&self->lock_lock) < 0) {
221+
PyErr_SetString(ThreadError, "failed to reinitialize lock at fork");
222+
return NULL;
223+
}
224+
225+
self->locked = 0;
226+
227+
Py_RETURN_NONE;
228+
}
229+
#endif /* HAVE_FORK */
230+
231+
216232
static PyMethodDef lock_methods[] = {
217233
{"acquire_lock", (PyCFunction)(void(*)(void))lock_PyThread_acquire_lock,
218234
METH_VARARGS | METH_KEYWORDS, acquire_doc},
@@ -230,6 +246,10 @@ static PyMethodDef lock_methods[] = {
230246
METH_VARARGS | METH_KEYWORDS, acquire_doc},
231247
{"__exit__", (PyCFunction)lock_PyThread_release_lock,
232248
METH_VARARGS, release_doc},
249+
#ifdef HAVE_FORK
250+
{"_at_fork_reinit", (PyCFunction)lock__at_fork_reinit,
251+
METH_NOARGS, NULL},
252+
#endif
233253
{NULL, NULL} /* sentinel */
234254
};
235255

@@ -446,22 +466,20 @@ For internal use by `threading.Condition`.");
446466
static PyObject *
447467
rlock_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
448468
{
449-
rlockobject *self;
450-
451-
self = (rlockobject *) type->tp_alloc(type, 0);
452-
if (self != NULL) {
453-
self->in_weakreflist = NULL;
454-
self->rlock_owner = 0;
455-
self->rlock_count = 0;
456-
457-
self->rlock_lock = PyThread_allocate_lock();
458-
if (self->rlock_lock == NULL) {
459-
Py_DECREF(self);
460-
PyErr_SetString(ThreadError, "can't allocate lock");
461-
return NULL;
462-
}
469+
rlockobject *self = (rlockobject *) type->tp_alloc(type, 0);
470+
if (self == NULL) {
471+
return NULL;
463472
}
473+
self->in_weakreflist = NULL;
474+
self->rlock_owner = 0;
475+
self->rlock_count = 0;
464476

477+
self->rlock_lock = PyThread_allocate_lock();
478+
if (self->rlock_lock == NULL) {
479+
Py_DECREF(self);
480+
PyErr_SetString(ThreadError, "can't allocate lock");
481+
return NULL;
482+
}
465483
return (PyObject *) self;
466484
}
467485

@@ -475,6 +493,23 @@ rlock_repr(rlockobject *self)
475493
}
476494

477495

496+
#ifdef HAVE_FORK
497+
static PyObject *
498+
rlock__at_fork_reinit(rlockobject *self, PyObject *Py_UNUSED(args))
499+
{
500+
if (_PyThread_at_fork_reinit(&self->rlock_lock) < 0) {
501+
PyErr_SetString(ThreadError, "failed to reinitialize lock at fork");
502+
return NULL;
503+
}
504+
505+
self->rlock_owner = 0;
506+
self->rlock_count = 0;
507+
508+
Py_RETURN_NONE;
509+
}
510+
#endif /* HAVE_FORK */
511+
512+
478513
static PyMethodDef rlock_methods[] = {
479514
{"acquire", (PyCFunction)(void(*)(void))rlock_acquire,
480515
METH_VARARGS | METH_KEYWORDS, rlock_acquire_doc},
@@ -490,6 +525,10 @@ static PyMethodDef rlock_methods[] = {
490525
METH_VARARGS | METH_KEYWORDS, rlock_acquire_doc},
491526
{"__exit__", (PyCFunction)rlock_release,
492527
METH_VARARGS, rlock_release_doc},
528+
#ifdef HAVE_FORK
529+
{"_at_fork_reinit", (PyCFunction)rlock__at_fork_reinit,
530+
METH_NOARGS, NULL},
531+
#endif
493532
{NULL, NULL} /* sentinel */
494533
};
495534

Modules/posixmodule.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,8 @@ register_at_forker(PyObject **lst, PyObject *func)
491491
}
492492
return PyList_Append(*lst, func);
493493
}
494-
#endif
494+
#endif /* HAVE_FORK */
495+
495496

496497
/* Legacy wrapper */
497498
void

Python/thread_pthread.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,26 @@ PyThread_release_lock(PyThread_type_lock lock)
693693

694694
#endif /* USE_SEMAPHORES */
695695

696+
int
697+
_PyThread_at_fork_reinit(PyThread_type_lock *lock)
698+
{
699+
PyThread_type_lock new_lock = PyThread_allocate_lock();
700+
if (new_lock == NULL) {
701+
return -1;
702+
}
703+
704+
/* bpo-6721, bpo-40089: The old lock can be in an inconsistent state.
705+
fork() can be called in the middle of an operation on the lock done by
706+
another thread. So don't call PyThread_free_lock(*lock).
707+
708+
Leak memory on purpose. Don't release the memory either since the
709+
address of a mutex is relevant. Putting two mutexes at the same address
710+
can lead to problems. */
711+
712+
*lock = new_lock;
713+
return 0;
714+
}
715+
696716
int
697717
PyThread_acquire_lock(PyThread_type_lock lock, int waitflag)
698718
{

0 commit comments

Comments
 (0)