Skip to content

Commit 7c3e577

Browse files
committed
Issue python#7316: the acquire() method of lock objects in the :mod:threading
module now takes an optional timeout argument in seconds. Timeout support relies on the system threading library, so as to avoid a semi-busy wait loop.
1 parent e53de3d commit 7c3e577

File tree

11 files changed

+326
-77
lines changed

11 files changed

+326
-77
lines changed

Doc/library/_thread.rst

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ implementation. For systems lacking the :mod:`_thread` module, the
2828
:mod:`_dummy_thread` module is available. It duplicates this module's interface
2929
and can be used as a drop-in replacement.
3030

31-
It defines the following constant and functions:
31+
It defines the following constants and functions:
3232

3333

3434
.. exception:: error
@@ -103,19 +103,34 @@ It defines the following constant and functions:
103103
Availability: Windows, systems with POSIX threads.
104104

105105

106+
.. data:: TIMEOUT_MAX
107+
108+
The maximum value allowed for the *timeout* parameter of
109+
:meth:`Lock.acquire`. Specifiying a timeout greater than this value will
110+
raise an :exc:`OverflowError`.
111+
112+
106113
Lock objects have the following methods:
107114

108115

109-
.. method:: lock.acquire([waitflag])
116+
.. method:: lock.acquire(waitflag=1, timeout=-1)
110117

111-
Without the optional argument, this method acquires the lock unconditionally, if
118+
Without any optional argument, this method acquires the lock unconditionally, if
112119
necessary waiting until it is released by another thread (only one thread at a
113-
time can acquire a lock --- that's their reason for existence). If the integer
114-
*waitflag* argument is present, the action depends on its value: if it is zero,
115-
the lock is only acquired if it can be acquired immediately without waiting,
116-
while if it is nonzero, the lock is acquired unconditionally as before. The
117-
return value is ``True`` if the lock is acquired successfully, ``False`` if not.
120+
time can acquire a lock --- that's their reason for existence).
118121

122+
If the integer *waitflag* argument is present, the action depends on its
123+
value: if it is zero, the lock is only acquired if it can be acquired
124+
immediately without waiting, while if it is nonzero, the lock is acquired
125+
unconditionally as above.
126+
127+
If the floating-point *timeout* argument is present and positive, it
128+
specifies the maximum wait time in seconds before returning. A negative
129+
*timeout* argument specifies an unbounded wait. You cannot specify
130+
a *timeout* if *waitflag* is zero.
131+
132+
The return value is ``True`` if the lock is acquired successfully,
133+
``False`` if not.
119134

120135
.. method:: lock.release()
121136

Doc/library/threading.rst

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,16 @@ This module defines the following functions and objects:
155155
Availability: Windows, systems with POSIX threads.
156156

157157

158+
This module also defines the following constant:
159+
160+
.. data:: TIMEOUT_MAX
161+
162+
The maximum value allowed for the *timeout* parameter of blocking functions
163+
(:meth:`Lock.acquire`, :meth:`RLock.acquire`, :meth:`Condition.wait`, etc.).
164+
Specifiying a timeout greater than this value will raise an
165+
:exc:`OverflowError`.
166+
167+
158168
Detailed interfaces for the objects are documented below.
159169

160170
The design of this module is loosely based on Java's threading model. However,
@@ -349,7 +359,7 @@ and may vary across implementations.
349359
All methods are executed atomically.
350360

351361

352-
.. method:: Lock.acquire(blocking=True)
362+
.. method:: Lock.acquire(blocking=True, timeout=-1)
353363

354364
Acquire a lock, blocking or non-blocking.
355365

@@ -363,6 +373,15 @@ All methods are executed atomically.
363373
without an argument would block, return false immediately; otherwise, do the
364374
same thing as when called without arguments, and return true.
365375

376+
When invoked with the floating-point *timeout* argument set to a positive
377+
value, block for at most the number of seconds specified by *timeout*
378+
and as long as the lock cannot be acquired. A negative *timeout* argument
379+
specifies an unbounded wait. It is forbidden to specify a *timeout*
380+
when *blocking* is false.
381+
382+
The return value is ``True`` if the lock is acquired successfully,
383+
``False`` if not (for example if the *timeout* expired).
384+
366385

367386
.. method:: Lock.release()
368387

@@ -396,7 +415,7 @@ pair) resets the lock to unlocked and allows another thread blocked in
396415
:meth:`acquire` to proceed.
397416

398417

399-
.. method:: RLock.acquire(blocking=True)
418+
.. method:: RLock.acquire(blocking=True, timeout=-1)
400419

401420
Acquire a lock, blocking or non-blocking.
402421

@@ -415,6 +434,11 @@ pair) resets the lock to unlocked and allows another thread blocked in
415434
without an argument would block, return false immediately; otherwise, do the
416435
same thing as when called without arguments, and return true.
417436

437+
When invoked with the floating-point *timeout* argument set to a positive
438+
value, block for at most the number of seconds specified by *timeout*
439+
and as long as the lock cannot be acquired. Return true if the lock has
440+
been acquired, false if the timeout has elapsed.
441+
418442

419443
.. method:: RLock.release()
420444

Include/pythread.h

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,41 @@ PyAPI_FUNC(void) PyThread_free_lock(PyThread_type_lock);
1919
PyAPI_FUNC(int) PyThread_acquire_lock(PyThread_type_lock, int);
2020
#define WAIT_LOCK 1
2121
#define NOWAIT_LOCK 0
22+
23+
/* PY_TIMEOUT_T is the integral type used to specify timeouts when waiting
24+
on a lock (see PyThread_acquire_lock_timed() below).
25+
PY_TIMEOUT_MAX is the highest usable value (in microseconds) of that
26+
type, and depends on the system threading API.
27+
28+
NOTE: this isn't the same value as `_thread.TIMEOUT_MAX`. The _thread
29+
module exposes a higher-level API, with timeouts expressed in seconds
30+
and floating-point numbers allowed.
31+
*/
32+
#if defined(HAVE_LONG_LONG)
33+
#define PY_TIMEOUT_T PY_LONG_LONG
34+
#define PY_TIMEOUT_MAX PY_LLONG_MAX
35+
#else
36+
#define PY_TIMEOUT_T long
37+
#define PY_TIMEOUT_MAX LONG_MAX
38+
#endif
39+
40+
/* In the NT API, the timeout is a DWORD and is expressed in milliseconds */
41+
#if defined (NT_THREADS)
42+
#if (0xFFFFFFFFLL * 1000 < PY_TIMEOUT_MAX)
43+
#undef PY_TIMEOUT_MAX
44+
#define PY_TIMEOUT_MAX (0xFFFFFFFFLL * 1000)
45+
#endif
46+
#endif
47+
48+
/* If microseconds == 0, the call is non-blocking: it returns immediately
49+
even when the lock can't be acquired.
50+
If microseconds > 0, the call waits up to the specified duration.
51+
If microseconds < 0, the call waits until success (or abnormal failure)
52+
53+
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);
2257
PyAPI_FUNC(void) PyThread_release_lock(PyThread_type_lock);
2358

2459
PyAPI_FUNC(size_t) PyThread_get_stacksize(void);

Lib/_dummy_thread.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
'interrupt_main', 'LockType']
1818

1919
import traceback as _traceback
20+
import time
21+
22+
# A dummy value
23+
TIMEOUT_MAX = 2**31
2024

2125
class error(Exception):
2226
"""Dummy implementation of _thread.error."""
@@ -92,7 +96,7 @@ class LockType(object):
9296
def __init__(self):
9397
self.locked_status = False
9498

95-
def acquire(self, waitflag=None):
99+
def acquire(self, waitflag=None, timeout=-1):
96100
"""Dummy implementation of acquire().
97101
98102
For blocking calls, self.locked_status is automatically set to
@@ -111,6 +115,8 @@ def acquire(self, waitflag=None):
111115
self.locked_status = True
112116
return True
113117
else:
118+
if timeout > 0:
119+
time.sleep(timeout)
114120
return False
115121

116122
__enter__ = acquire

Lib/multiprocessing/pool.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -440,10 +440,10 @@ def _terminate_pool(cls, taskqueue, inqueue, outqueue, pool,
440440
p.terminate()
441441

442442
debug('joining task handler')
443-
task_handler.join(1e100)
443+
task_handler.join()
444444

445445
debug('joining result handler')
446-
result_handler.join(1e100)
446+
task_handler.join()
447447

448448
if pool and hasattr(pool[0], 'terminate'):
449449
debug('joining pool workers')

Lib/test/lock_tests.py

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import sys
66
import time
7-
from _thread import start_new_thread, get_ident
7+
from _thread import start_new_thread, get_ident, TIMEOUT_MAX
88
import threading
99
import unittest
1010

@@ -62,6 +62,14 @@ def tearDown(self):
6262
support.threading_cleanup(*self._threads)
6363
support.reap_children()
6464

65+
def assertTimeout(self, actual, expected):
66+
# The waiting and/or time.time() can be imprecise, which
67+
# is why comparing to the expected value would sometimes fail
68+
# (especially under Windows).
69+
self.assertGreaterEqual(actual, expected * 0.6)
70+
# Test nothing insane happened
71+
self.assertLess(actual, expected * 10.0)
72+
6573

6674
class BaseLockTests(BaseTestCase):
6775
"""
@@ -143,6 +151,32 @@ def f():
143151
Bunch(f, 15).wait_for_finished()
144152
self.assertEqual(n, len(threading.enumerate()))
145153

154+
def test_timeout(self):
155+
lock = self.locktype()
156+
# Can't set timeout if not blocking
157+
self.assertRaises(ValueError, lock.acquire, 0, 1)
158+
# Invalid timeout values
159+
self.assertRaises(ValueError, lock.acquire, timeout=-100)
160+
self.assertRaises(OverflowError, lock.acquire, timeout=1e100)
161+
self.assertRaises(OverflowError, lock.acquire, timeout=TIMEOUT_MAX + 1)
162+
# TIMEOUT_MAX is ok
163+
lock.acquire(timeout=TIMEOUT_MAX)
164+
lock.release()
165+
t1 = time.time()
166+
self.assertTrue(lock.acquire(timeout=5))
167+
t2 = time.time()
168+
# Just a sanity test that it didn't actually wait for the timeout.
169+
self.assertLess(t2 - t1, 5)
170+
results = []
171+
def f():
172+
t1 = time.time()
173+
results.append(lock.acquire(timeout=0.5))
174+
t2 = time.time()
175+
results.append(t2 - t1)
176+
Bunch(f, 1).wait_for_finished()
177+
self.assertFalse(results[0])
178+
self.assertTimeout(results[1], 0.5)
179+
146180

147181
class LockTests(BaseLockTests):
148182
"""
@@ -284,14 +318,14 @@ def test_timeout(self):
284318
def f():
285319
results1.append(evt.wait(0.0))
286320
t1 = time.time()
287-
r = evt.wait(0.2)
321+
r = evt.wait(0.5)
288322
t2 = time.time()
289323
results2.append((r, t2 - t1))
290324
Bunch(f, N).wait_for_finished()
291325
self.assertEqual(results1, [False] * N)
292326
for r, dt in results2:
293327
self.assertFalse(r)
294-
self.assertTrue(dt >= 0.2, dt)
328+
self.assertTimeout(dt, 0.5)
295329
# The event is set
296330
results1 = []
297331
results2 = []
@@ -397,14 +431,14 @@ def test_timeout(self):
397431
def f():
398432
cond.acquire()
399433
t1 = time.time()
400-
cond.wait(0.2)
434+
cond.wait(0.5)
401435
t2 = time.time()
402436
cond.release()
403437
results.append(t2 - t1)
404438
Bunch(f, N).wait_for_finished()
405439
self.assertEqual(len(results), 5)
406440
for dt in results:
407-
self.assertTrue(dt >= 0.2, dt)
441+
self.assertTimeout(dt, 0.5)
408442

409443

410444
class BaseSemaphoreTests(BaseTestCase):

Lib/threading.py

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
_CRLock = _thread.RLock
3232
except AttributeError:
3333
_CRLock = None
34+
TIMEOUT_MAX = _thread.TIMEOUT_MAX
3435
del _thread
3536

3637

@@ -107,14 +108,14 @@ def __repr__(self):
107108
return "<%s owner=%r count=%d>" % (
108109
self.__class__.__name__, owner, self._count)
109110

110-
def acquire(self, blocking=True):
111+
def acquire(self, blocking=True, timeout=-1):
111112
me = _get_ident()
112113
if self._owner == me:
113114
self._count = self._count + 1
114115
if __debug__:
115116
self._note("%s.acquire(%s): recursive success", self, blocking)
116117
return 1
117-
rc = self._block.acquire(blocking)
118+
rc = self._block.acquire(blocking, timeout)
118119
if rc:
119120
self._owner = me
120121
self._count = 1
@@ -234,22 +235,10 @@ def wait(self, timeout=None):
234235
if __debug__:
235236
self._note("%s.wait(): got it", self)
236237
else:
237-
# Balancing act: We can't afford a pure busy loop, so we
238-
# have to sleep; but if we sleep the whole timeout time,
239-
# we'll be unresponsive. The scheme here sleeps very
240-
# little at first, longer as time goes on, but never longer
241-
# than 20 times per second (or the timeout time remaining).
242-
endtime = _time() + timeout
243-
delay = 0.0005 # 500 us -> initial delay of 1 ms
244-
while True:
245-
gotit = waiter.acquire(0)
246-
if gotit:
247-
break
248-
remaining = endtime - _time()
249-
if remaining <= 0:
250-
break
251-
delay = min(delay * 2, remaining, .05)
252-
_sleep(delay)
238+
if timeout > 0:
239+
gotit = waiter.acquire(True, timeout)
240+
else:
241+
gotit = waiter.acquire(False)
253242
if not gotit:
254243
if __debug__:
255244
self._note("%s.wait(%s): timed out", self, timeout)

Misc/NEWS

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,11 @@ C-API
312312
Library
313313
-------
314314

315+
- Issue #7316: the acquire() method of lock objects in the :mod:`threading`
316+
module now takes an optional timeout argument in seconds. Timeout support
317+
relies on the system threading library, so as to avoid a semi-busy wait
318+
loop.
319+
315320
- Issue #8383: pickle and pickletools use surrogatepass error handler when
316321
encoding unicode as utf8 to support lone surrogates and stay compatible with
317322
Python 2.x and 3.0

0 commit comments

Comments
 (0)