Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 5af0e41

Browse files
committed
Bug #788520: Queue class has logic error when non-blocking
I don't agree it had a bug (see the report), so this is *not* a candidate for backporting, but the docs were confusing and the Queue implementation was old enough to vote. Rewrote put/put_nowait/get/get_nowait from scratch, to use a pair of Conditions (not_full and not_empty), sharing a common mutex. The code is 1/4 the size now, and 6.25x easier to understand. For blocking with timeout, we also get to reuse (indirectly) the tedious timeout code from threading.Condition. The Full and Empty exceptions raised by non-blocking calls are now easy (instead of nearly impossible) to explain truthfully: Full is raised if and only if the Queue truly is full when the non-blocking put call checks the queue size, and similarly for Empty versus non-blocking get. What I don't know is whether the new implementation is slower (or faster) than the old one. I don't really care. Anyone who cares a lot is encouraged to check that.
1 parent 183dabc commit 5af0e41

3 files changed

Lines changed: 76 additions & 88 deletions

File tree

Doc/lib/libqueue.tex

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,13 @@ \section{\module{Queue} ---
2626
\begin{excdesc}{Empty}
2727
Exception raised when non-blocking \method{get()} (or
2828
\method{get_nowait()}) is called on a \class{Queue} object which is
29-
empty or locked.
29+
empty.
3030
\end{excdesc}
3131

3232
\begin{excdesc}{Full}
3333
Exception raised when non-blocking \method{put()} (or
3434
\method{put_nowait()}) is called on a \class{Queue} object which is
35-
full or locked.
35+
full.
3636
\end{excdesc}
3737

3838
\subsection{Queue Objects}
@@ -51,7 +51,7 @@ \subsection{Queue Objects}
5151

5252
\begin{methoddesc}{empty}{}
5353
Return \code{True} if the queue is empty, \code{False} otherwise.
54-
Becauseof multithreading semantics, this is not reliable.
54+
Because of multithreading semantics, this is not reliable.
5555
\end{methoddesc}
5656

5757
\begin{methoddesc}{full}{}

Lib/Queue.py

Lines changed: 62 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""A multi-producer, multi-consumer queue."""
22

3-
from time import time as _time, sleep as _sleep
3+
from time import time as _time
44
from collections import deque
55

66
__all__ = ['Empty', 'Full', 'Queue']
@@ -20,14 +20,21 @@ def __init__(self, maxsize=0):
2020
If maxsize is <= 0, the queue size is infinite.
2121
"""
2222
try:
23-
import thread
23+
import threading
2424
except ImportError:
25-
import dummy_thread as thread
25+
import dummy_threading as threading
2626
self._init(maxsize)
27-
self.mutex = thread.allocate_lock()
28-
self.esema = thread.allocate_lock()
29-
self.esema.acquire()
30-
self.fsema = thread.allocate_lock()
27+
# mutex must be held whenever the queue is mutating. All methods
28+
# that acquire mutex must release it before returning. mutex
29+
# is shared between the two conditions, so acquiring and
30+
# releasing the conditions also acquires and releases mutex.
31+
self.mutex = threading.Lock()
32+
# Notify not_empty whenever an item is added to the queue; a
33+
# thread waiting to get is notified then.
34+
self.not_empty = threading.Condition(self.mutex)
35+
# Notify not_full whenever an item is removed from the queue;
36+
# a thread waiting to put is notified then.
37+
self.not_full = threading.Condition(self.mutex)
3138

3239
def qsize(self):
3340
"""Return the approximate size of the queue (not reliable!)."""
@@ -61,59 +68,42 @@ def put(self, item, block=True, timeout=None):
6168
is immediately available, else raise the Full exception ('timeout'
6269
is ignored in that case).
6370
"""
64-
if block:
71+
if not block:
72+
return self.put_nowait(item)
73+
self.not_full.acquire()
74+
try:
6575
if timeout is None:
66-
# blocking, w/o timeout, i.e. forever
67-
self.fsema.acquire()
68-
elif timeout >= 0:
69-
# waiting max. 'timeout' seconds.
70-
# this code snipped is from threading.py: _Event.wait():
71-
# Balancing act: We can't afford a pure busy loop, so we
72-
# have to sleep; but if we sleep the whole timeout time,
73-
# we'll be unresponsive. The scheme here sleeps very
74-
# little at first, longer as time goes on, but never longer
75-
# than 20 times per second (or the timeout time remaining).
76-
delay = 0.0005 # 500 us -> initial delay of 1 ms
76+
while self._full():
77+
self.not_full.wait()
78+
else:
79+
if timeout < 0:
80+
raise ValueError("'timeout' must be a positive number")
7781
endtime = _time() + timeout
78-
while True:
79-
if self.fsema.acquire(0):
80-
break
82+
while self._full():
8183
remaining = endtime - _time()
82-
if remaining <= 0: #time is over and no slot was free
84+
if remaining < 0.0:
8385
raise Full
84-
delay = min(delay * 2, remaining, .05)
85-
_sleep(delay) #reduce CPU usage by using a sleep
86-
else:
87-
raise ValueError("'timeout' must be a positive number")
88-
elif not self.fsema.acquire(0):
89-
raise Full
90-
self.mutex.acquire()
91-
release_fsema = True
92-
try:
93-
was_empty = self._empty()
86+
self.not_full.wait(remaining)
9487
self._put(item)
95-
# If we fail before here, the empty state has
96-
# not changed, so we can skip the release of esema
97-
if was_empty:
98-
self.esema.release()
99-
# If we fail before here, the queue can not be full, so
100-
# release_full_sema remains True
101-
release_fsema = not self._full()
88+
self.not_empty.notify()
10289
finally:
103-
# Catching system level exceptions here (RecursionDepth,
104-
# OutOfMemory, etc) - so do as little as possible in terms
105-
# of Python calls.
106-
if release_fsema:
107-
self.fsema.release()
108-
self.mutex.release()
90+
self.not_full.release()
10991

11092
def put_nowait(self, item):
11193
"""Put an item into the queue without blocking.
11294
11395
Only enqueue the item if a free slot is immediately available.
11496
Otherwise raise the Full exception.
11597
"""
116-
return self.put(item, False)
98+
self.not_full.acquire()
99+
try:
100+
if self._full():
101+
raise Full
102+
else:
103+
self._put(item)
104+
self.not_empty.notify()
105+
finally:
106+
self.not_full.release()
117107

118108
def get(self, block=True, timeout=None):
119109
"""Remove and return an item from the queue.
@@ -126,57 +116,44 @@ def get(self, block=True, timeout=None):
126116
available, else raise the Empty exception ('timeout' is ignored
127117
in that case).
128118
"""
129-
if block:
119+
if not block:
120+
return self.get_nowait()
121+
self.not_empty.acquire()
122+
try:
130123
if timeout is None:
131-
# blocking, w/o timeout, i.e. forever
132-
self.esema.acquire()
133-
elif timeout >= 0:
134-
# waiting max. 'timeout' seconds.
135-
# this code snipped is from threading.py: _Event.wait():
136-
# Balancing act: We can't afford a pure busy loop, so we
137-
# have to sleep; but if we sleep the whole timeout time,
138-
# we'll be unresponsive. The scheme here sleeps very
139-
# little at first, longer as time goes on, but never longer
140-
# than 20 times per second (or the timeout time remaining).
141-
delay = 0.0005 # 500 us -> initial delay of 1 ms
124+
while self._empty():
125+
self.not_empty.wait()
126+
else:
127+
if timeout < 0:
128+
raise ValueError("'timeout' must be a positive number")
142129
endtime = _time() + timeout
143-
while 1:
144-
if self.esema.acquire(0):
145-
break
130+
while self._empty():
146131
remaining = endtime - _time()
147-
if remaining <= 0: #time is over and no element arrived
132+
if remaining < 0.0:
148133
raise Empty
149-
delay = min(delay * 2, remaining, .05)
150-
_sleep(delay) #reduce CPU usage by using a sleep
151-
else:
152-
raise ValueError("'timeout' must be a positive number")
153-
elif not self.esema.acquire(0):
154-
raise Empty
155-
self.mutex.acquire()
156-
release_esema = True
157-
try:
158-
was_full = self._full()
134+
self.not_empty.wait(remaining)
159135
item = self._get()
160-
# If we fail before here, the full state has
161-
# not changed, so we can skip the release of fsema
162-
if was_full:
163-
self.fsema.release()
164-
# Failure means empty state also unchanged - release_esema
165-
# remains True.
166-
release_esema = not self._empty()
136+
self.not_full.notify()
137+
return item
167138
finally:
168-
if release_esema:
169-
self.esema.release()
170-
self.mutex.release()
171-
return item
139+
self.not_empty.release()
172140

173141
def get_nowait(self):
174142
"""Remove and return an item from the queue without blocking.
175143
176144
Only get an item if one is immediately available. Otherwise
177145
raise the Empty exception.
178146
"""
179-
return self.get(False)
147+
self.not_empty.acquire()
148+
try:
149+
if self._empty():
150+
raise Empty
151+
else:
152+
item = self._get()
153+
self.not_full.notify()
154+
return item
155+
finally:
156+
self.not_empty.release()
180157

181158
# Override these methods to implement other queue organizations
182159
# (e.g. stack or priority queue).

Misc/NEWS

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,17 @@ Extension modules
2929
Library
3030
-------
3131

32+
- Bug #788520. Queue.{get, get_nowait, put, put_nowait} have new
33+
implementations, exploiting Conditions (which didn't exist at the time
34+
Queue was introduced). A minor semantic change is that the Full and
35+
Empty exceptions raised by non-blocking calls now occur only if the
36+
queue truly was full or empty at the instant the queue was checked (of
37+
course the Queue may no longer be full or empty by the time a calling
38+
thread sees those exceptions, though). Before, the exceptions could
39+
also be raised if it was "merely inconvenient" for the implementation
40+
to determine the true state of the Queue (because the Queue was locked
41+
by some other method in progress).
42+
3243
- Bugs #979794 and #980117: difflib.get_grouped_opcodes() now handles the
3344
case of comparing two empty lists. This affected both context_diff() and
3445
unified_diff(),

0 commit comments

Comments
 (0)