Skip to content

Commit d913d1c

Browse files
authored
Fix waiter cancellation in asyncio.Lock (#1031) (#2038)
Avoid a deadlock when the waiter who is about to take the lock is cancelled Issue #27585
1 parent 3fc2fa8 commit d913d1c

File tree

3 files changed

+37
-5
lines changed

3 files changed

+37
-5
lines changed

Lib/asyncio/locks.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,10 @@ def acquire(self):
176176
yield from fut
177177
self._locked = True
178178
return True
179+
except futures.CancelledError:
180+
if not self._locked:
181+
self._wake_up_first()
182+
raise
179183
finally:
180184
self._waiters.remove(fut)
181185

@@ -192,14 +196,17 @@ def release(self):
192196
"""
193197
if self._locked:
194198
self._locked = False
195-
# Wake up the first waiter who isn't cancelled.
196-
for fut in self._waiters:
197-
if not fut.done():
198-
fut.set_result(True)
199-
break
199+
self._wake_up_first()
200200
else:
201201
raise RuntimeError('Lock is not acquired.')
202202

203+
def _wake_up_first(self):
204+
"""Wake up the first waiter who isn't cancelled."""
205+
for fut in self._waiters:
206+
if not fut.done():
207+
fut.set_result(True)
208+
break
209+
203210

204211
class Event:
205212
"""Asynchronous equivalent to threading.Event.

Lib/test/test_asyncio/test_locks.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,28 @@ def lockit(name, blocker):
176176
self.assertTrue(tb.cancelled())
177177
self.assertTrue(tc.done())
178178

179+
def test_finished_waiter_cancelled(self):
180+
lock = asyncio.Lock(loop=self.loop)
181+
182+
ta = asyncio.Task(lock.acquire(), loop=self.loop)
183+
test_utils.run_briefly(self.loop)
184+
self.assertTrue(lock.locked())
185+
186+
tb = asyncio.Task(lock.acquire(), loop=self.loop)
187+
test_utils.run_briefly(self.loop)
188+
self.assertEqual(len(lock._waiters), 1)
189+
190+
# Create a second waiter, wake up the first, and cancel it.
191+
# Without the fix, the second was not woken up.
192+
tc = asyncio.Task(lock.acquire(), loop=self.loop)
193+
lock.release()
194+
tb.cancel()
195+
test_utils.run_briefly(self.loop)
196+
197+
self.assertTrue(lock.locked())
198+
self.assertTrue(ta.done())
199+
self.assertTrue(tb.cancelled())
200+
179201
def test_release_not_acquired(self):
180202
lock = asyncio.Lock(loop=self.loop)
181203

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ Extension Modules
5656
Library
5757
-------
5858

59+
- bpo-27585: Fix waiter cancellation in asyncio.Lock.
60+
Patch by Mathieu Sornay.
61+
5962
- bpo-30418: On Windows, subprocess.Popen.communicate() now also ignore EINVAL
6063
on stdin.write() if the child process is still running but closed the pipe.
6164

0 commit comments

Comments
 (0)