Skip to content

Commit 810023d

Browse files
committed
Issue python#8844: Regular and recursive lock acquisitions can now be interrupted
by signals on platforms using pthreads. Patch by Reid Kleckner.
1 parent 119cda0 commit 810023d

File tree

10 files changed

+299
-72
lines changed

10 files changed

+299
-72
lines changed

Doc/library/_thread.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ Lock objects have the following methods:
137137
.. versionchanged:: 3.2
138138
The *timeout* parameter is new.
139139

140+
.. versionchanged:: 3.2
141+
Lock acquires can now be interrupted by signals on POSIX.
142+
143+
140144
.. method:: lock.release()
141145

142146
Releases the lock. The lock must have been acquired earlier, but not

Doc/library/threading.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,9 @@ All methods are executed atomically.
408408
.. versionchanged:: 3.2
409409
The *timeout* parameter is new.
410410

411+
.. versionchanged:: 3.2
412+
Lock acquires can now be interrupted by signals on POSIX.
413+
411414

412415
.. method:: Lock.release()
413416

Doc/whatsnew/3.2.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1212,6 +1212,12 @@ Multi-threading
12121212
* Similarly, :meth:`threading.Semaphore.acquire` also gained a *timeout*
12131213
argument. (Contributed by Torsten Landschoff; :issue:`850728`.)
12141214

1215+
* Regular and recursive lock acquisitions can now be interrupted by signals on
1216+
platforms using pthreads. This means that Python programs that deadlock while
1217+
acquiring locks can be successfully killed by repeatedly sending SIGINT to the
1218+
process (ie, by pressing Ctl+C in most shells).
1219+
(Contributed by Reid Kleckner; :issue:`8844`.)
1220+
12151221

12161222
Optimizations
12171223
=============

Include/pythread.h

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ typedef void *PyThread_type_sema;
99
extern "C" {
1010
#endif
1111

12+
/* Return status codes for Python lock acquisition. Chosen for maximum
13+
* backwards compatibility, ie failure -> 0, success -> 1. */
14+
typedef enum PyLockStatus {
15+
PY_LOCK_FAILURE = 0,
16+
PY_LOCK_ACQUIRED = 1,
17+
PY_LOCK_INTR
18+
} PyLockStatus;
19+
1220
PyAPI_FUNC(void) PyThread_init_thread(void);
1321
PyAPI_FUNC(long) PyThread_start_new_thread(void (*)(void *), void *);
1422
PyAPI_FUNC(void) PyThread_exit_thread(void);
@@ -49,11 +57,18 @@ PyAPI_FUNC(int) PyThread_acquire_lock(PyThread_type_lock, int);
4957
even when the lock can't be acquired.
5058
If microseconds > 0, the call waits up to the specified duration.
5159
If microseconds < 0, the call waits until success (or abnormal failure)
52-
60+
5361
microseconds must be less than PY_TIMEOUT_MAX. Behaviour otherwise is
54-
undefined. */
55-
PyAPI_FUNC(int) PyThread_acquire_lock_timed(PyThread_type_lock,
56-
PY_TIMEOUT_T microseconds);
62+
undefined.
63+
64+
If intr_flag is true and the acquire is interrupted by a signal, then the
65+
call will return PY_LOCK_INTR. The caller may reattempt to acquire the
66+
lock.
67+
*/
68+
PyAPI_FUNC(PyLockStatus) PyThread_acquire_lock_timed(PyThread_type_lock,
69+
PY_TIMEOUT_T microseconds,
70+
int intr_flag);
71+
5772
PyAPI_FUNC(void) PyThread_release_lock(PyThread_type_lock);
5873

5974
PyAPI_FUNC(size_t) PyThread_get_stacksize(void);

Lib/test/test_threadsignals.py

Lines changed: 115 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import sys
77
from test.support import run_unittest, import_module
88
thread = import_module('_thread')
9+
import time
910

1011
if sys.platform[:3] in ('win', 'os2') or sys.platform=='riscos':
1112
raise unittest.SkipTest("Can't test signal on %s" % sys.platform)
@@ -34,12 +35,12 @@ def send_signals():
3435
signalled_all.release()
3536

3637
class ThreadSignals(unittest.TestCase):
37-
"""Test signal handling semantics of threads.
38-
We spawn a thread, have the thread send two signals, and
39-
wait for it to finish. Check that we got both signals
40-
and that they were run by the main thread.
41-
"""
38+
4239
def test_signals(self):
40+
# Test signal handling semantics of threads.
41+
# We spawn a thread, have the thread send two signals, and
42+
# wait for it to finish. Check that we got both signals
43+
# and that they were run by the main thread.
4344
signalled_all.acquire()
4445
self.spawnSignallingThread()
4546
signalled_all.acquire()
@@ -66,6 +67,115 @@ def test_signals(self):
6667
def spawnSignallingThread(self):
6768
thread.start_new_thread(send_signals, ())
6869

70+
def alarm_interrupt(self, sig, frame):
71+
raise KeyboardInterrupt
72+
73+
def test_lock_acquire_interruption(self):
74+
# Mimic receiving a SIGINT (KeyboardInterrupt) with SIGALRM while stuck
75+
# in a deadlock.
76+
oldalrm = signal.signal(signal.SIGALRM, self.alarm_interrupt)
77+
try:
78+
lock = thread.allocate_lock()
79+
lock.acquire()
80+
signal.alarm(1)
81+
self.assertRaises(KeyboardInterrupt, lock.acquire)
82+
finally:
83+
signal.signal(signal.SIGALRM, oldalrm)
84+
85+
def test_rlock_acquire_interruption(self):
86+
# Mimic receiving a SIGINT (KeyboardInterrupt) with SIGALRM while stuck
87+
# in a deadlock.
88+
oldalrm = signal.signal(signal.SIGALRM, self.alarm_interrupt)
89+
try:
90+
rlock = thread.RLock()
91+
# For reentrant locks, the initial acquisition must be in another
92+
# thread.
93+
def other_thread():
94+
rlock.acquire()
95+
thread.start_new_thread(other_thread, ())
96+
# Wait until we can't acquire it without blocking...
97+
while rlock.acquire(blocking=False):
98+
rlock.release()
99+
time.sleep(0.01)
100+
signal.alarm(1)
101+
self.assertRaises(KeyboardInterrupt, rlock.acquire)
102+
finally:
103+
signal.signal(signal.SIGALRM, oldalrm)
104+
105+
def acquire_retries_on_intr(self, lock):
106+
self.sig_recvd = False
107+
def my_handler(signal, frame):
108+
self.sig_recvd = True
109+
old_handler = signal.signal(signal.SIGUSR1, my_handler)
110+
try:
111+
def other_thread():
112+
# Acquire the lock in a non-main thread, so this test works for
113+
# RLocks.
114+
lock.acquire()
115+
# Wait until the main thread is blocked in the lock acquire, and
116+
# then wake it up with this.
117+
time.sleep(0.5)
118+
os.kill(process_pid, signal.SIGUSR1)
119+
# Let the main thread take the interrupt, handle it, and retry
120+
# the lock acquisition. Then we'll let it run.
121+
time.sleep(0.5)
122+
lock.release()
123+
thread.start_new_thread(other_thread, ())
124+
# Wait until we can't acquire it without blocking...
125+
while lock.acquire(blocking=False):
126+
lock.release()
127+
time.sleep(0.01)
128+
result = lock.acquire() # Block while we receive a signal.
129+
self.assertTrue(self.sig_recvd)
130+
self.assertTrue(result)
131+
finally:
132+
signal.signal(signal.SIGUSR1, old_handler)
133+
134+
def test_lock_acquire_retries_on_intr(self):
135+
self.acquire_retries_on_intr(thread.allocate_lock())
136+
137+
def test_rlock_acquire_retries_on_intr(self):
138+
self.acquire_retries_on_intr(thread.RLock())
139+
140+
def test_interrupted_timed_acquire(self):
141+
# Test to make sure we recompute lock acquisition timeouts when we
142+
# receive a signal. Check this by repeatedly interrupting a lock
143+
# acquire in the main thread, and make sure that the lock acquire times
144+
# out after the right amount of time.
145+
self.start = None
146+
self.end = None
147+
self.sigs_recvd = 0
148+
done = thread.allocate_lock()
149+
done.acquire()
150+
lock = thread.allocate_lock()
151+
lock.acquire()
152+
def my_handler(signum, frame):
153+
self.sigs_recvd += 1
154+
old_handler = signal.signal(signal.SIGUSR1, my_handler)
155+
try:
156+
def timed_acquire():
157+
self.start = time.time()
158+
lock.acquire(timeout=0.5)
159+
self.end = time.time()
160+
def send_signals():
161+
for _ in range(40):
162+
time.sleep(0.05)
163+
os.kill(process_pid, signal.SIGUSR1)
164+
done.release()
165+
166+
# Send the signals from the non-main thread, since the main thread
167+
# is the only one that can process signals.
168+
thread.start_new_thread(send_signals, ())
169+
timed_acquire()
170+
# Wait for thread to finish
171+
done.acquire()
172+
# This allows for some timing and scheduling imprecision
173+
self.assertLess(self.end - self.start, 2.0)
174+
self.assertGreater(self.end - self.start, 0.3)
175+
self.assertEqual(40, self.sigs_recvd)
176+
finally:
177+
signal.signal(signal.SIGUSR1, old_handler)
178+
69179

70180
def test_main():
71181
global signal_blackboard

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,7 @@ Paul Kippes
454454
Steve Kirsch
455455
Sebastian Kirsche
456456
Ron Klatchko
457+
Reid Kleckner
457458
Bastian Kleineidam
458459
Bob Kline
459460
Matthias Klose

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ What's New in Python 3.2 Beta 2?
1010
Core and Builtins
1111
-----------------
1212

13+
- Issue #8844: Regular and recursive lock acquisitions can now be interrupted
14+
by signals on platforms using pthreads. Patch by Reid Kleckner.
15+
1316
- Issue #4236: PyModule_Create2 now checks the import machinery directly
1417
rather than the Py_IsInitialized flag, avoiding a Fatal Python
1518
error in certain circumstances when an import is done in __del__.

Modules/_threadmodule.c

Lines changed: 67 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,66 @@ lock_dealloc(lockobject *self)
4040
PyObject_Del(self);
4141
}
4242

43+
/* Helper to acquire an interruptible lock with a timeout. If the lock acquire
44+
* is interrupted, signal handlers are run, and if they raise an exception,
45+
* PY_LOCK_INTR is returned. Otherwise, PY_LOCK_ACQUIRED or PY_LOCK_FAILURE
46+
* are returned, depending on whether the lock can be acquired withing the
47+
* timeout.
48+
*/
49+
static PyLockStatus
50+
acquire_timed(PyThread_type_lock lock, PY_TIMEOUT_T microseconds)
51+
{
52+
PyLockStatus r;
53+
_PyTime_timeval curtime;
54+
_PyTime_timeval endtime;
55+
56+
if (microseconds > 0) {
57+
_PyTime_gettimeofday(&endtime);
58+
endtime.tv_sec += microseconds / (1000 * 1000);
59+
endtime.tv_usec += microseconds % (1000 * 1000);
60+
}
61+
62+
63+
do {
64+
Py_BEGIN_ALLOW_THREADS
65+
r = PyThread_acquire_lock_timed(lock, microseconds, 1);
66+
Py_END_ALLOW_THREADS
67+
68+
if (r == PY_LOCK_INTR) {
69+
/* Run signal handlers if we were interrupted. Propagate
70+
* exceptions from signal handlers, such as KeyboardInterrupt, by
71+
* passing up PY_LOCK_INTR. */
72+
if (Py_MakePendingCalls() < 0) {
73+
return PY_LOCK_INTR;
74+
}
75+
76+
/* If we're using a timeout, recompute the timeout after processing
77+
* signals, since those can take time. */
78+
if (microseconds >= 0) {
79+
_PyTime_gettimeofday(&curtime);
80+
microseconds = ((endtime.tv_sec - curtime.tv_sec) * 1000000 +
81+
(endtime.tv_usec - curtime.tv_usec));
82+
83+
/* Check for negative values, since those mean block forever.
84+
*/
85+
if (microseconds <= 0) {
86+
r = PY_LOCK_FAILURE;
87+
}
88+
}
89+
}
90+
} while (r == PY_LOCK_INTR); /* Retry if we were interrupted. */
91+
92+
return r;
93+
}
94+
4395
static PyObject *
4496
lock_PyThread_acquire_lock(lockobject *self, PyObject *args, PyObject *kwds)
4597
{
4698
char *kwlist[] = {"blocking", "timeout", NULL};
4799
int blocking = 1;
48100
double timeout = -1;
49101
PY_TIMEOUT_T microseconds;
50-
int r;
102+
PyLockStatus r;
51103

52104
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|id:acquire", kwlist,
53105
&blocking, &timeout))
@@ -77,11 +129,12 @@ lock_PyThread_acquire_lock(lockobject *self, PyObject *args, PyObject *kwds)
77129
microseconds = (PY_TIMEOUT_T) timeout;
78130
}
79131

80-
Py_BEGIN_ALLOW_THREADS
81-
r = PyThread_acquire_lock_timed(self->lock_lock, microseconds);
82-
Py_END_ALLOW_THREADS
132+
r = acquire_timed(self->lock_lock, microseconds);
133+
if (r == PY_LOCK_INTR) {
134+
return NULL;
135+
}
83136

84-
return PyBool_FromLong(r);
137+
return PyBool_FromLong(r == PY_LOCK_ACQUIRED);
85138
}
86139

87140
PyDoc_STRVAR(acquire_doc,
@@ -93,7 +146,7 @@ locked (even by the same thread), waiting for another thread to release\n\
93146
the lock, and return None once the lock is acquired.\n\
94147
With an argument, this will only block if the argument is true,\n\
95148
and the return value reflects whether the lock is acquired.\n\
96-
The blocking operation is not interruptible.");
149+
The blocking operation is interruptible.");
97150

98151
static PyObject *
99152
lock_PyThread_release_lock(lockobject *self)
@@ -218,7 +271,7 @@ rlock_acquire(rlockobject *self, PyObject *args, PyObject *kwds)
218271
double timeout = -1;
219272
PY_TIMEOUT_T microseconds;
220273
long tid;
221-
int r = 1;
274+
PyLockStatus r = PY_LOCK_ACQUIRED;
222275

223276
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|id:acquire", kwlist,
224277
&blocking, &timeout))
@@ -265,17 +318,18 @@ rlock_acquire(rlockobject *self, PyObject *args, PyObject *kwds)
265318
if (microseconds == 0) {
266319
Py_RETURN_FALSE;
267320
}
268-
Py_BEGIN_ALLOW_THREADS
269-
r = PyThread_acquire_lock_timed(self->rlock_lock, microseconds);
270-
Py_END_ALLOW_THREADS
321+
r = acquire_timed(self->rlock_lock, microseconds);
271322
}
272-
if (r) {
323+
if (r == PY_LOCK_ACQUIRED) {
273324
assert(self->rlock_count == 0);
274325
self->rlock_owner = tid;
275326
self->rlock_count = 1;
276327
}
328+
else if (r == PY_LOCK_INTR) {
329+
return NULL;
330+
}
277331

278-
return PyBool_FromLong(r);
332+
return PyBool_FromLong(r == PY_LOCK_ACQUIRED);
279333
}
280334

281335
PyDoc_STRVAR(rlock_acquire_doc,
@@ -287,7 +341,7 @@ and another thread holds the lock, the method will return False\n\
287341
immediately. If `blocking` is True and another thread holds\n\
288342
the lock, the method will wait for the lock to be released,\n\
289343
take it and then return True.\n\
290-
(note: the blocking operation is not interruptible.)\n\
344+
(note: the blocking operation is interruptible.)\n\
291345
\n\
292346
In all other cases, the method will return True immediately.\n\
293347
Precisely, if the current thread already holds the lock, its\n\

0 commit comments

Comments
 (0)