Skip to content

Commit 5b19780

Browse files
committed
more lock thoughts
1 parent 6fad4ed commit 5b19780

File tree

2 files changed

+27
-24
lines changed

2 files changed

+27
-24
lines changed

_misc/_l.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,15 @@
2222

2323
min_res = time.get_clock_info("monotonic").resolution
2424

25-
start = time.monotonic_ns()
2625

27-
28-
async def check(lock: AsyncLock):
26+
async def check(lock: AsyncLock, start: int) -> tuple[int, int]:
2927
async with lock:
30-
v = max(random.random() / 1000, min_res)
28+
v = max(random.random() / 1e10, min_res)
3129
s = time.monotonic_ns()
3230
await asyncio.sleep(v)
3331
e = time.monotonic_ns()
34-
print(s - start, e - start, flush=True) # noqa: T201
3532
await asyncio.sleep(min_res)
33+
return (s - start, e - start)
3634

3735

3836
async def amain():
@@ -43,8 +41,11 @@ async def amain():
4341
for _ in range(10)
4442
for x in (True, False)
4543
]
46-
tsks = {loop.run(check(lock)) for loop in loops for _ in range(10)}
47-
await asyncio.gather(*tsks)
44+
start = time.monotonic_ns()
45+
tsks = {loop.run(check(lock, start)) for loop in loops for _ in range(10)}
46+
results = await asyncio.gather(*tsks)
47+
results.sort()
48+
print(*(f"{s} {e}" for s, e in results), sep="\n", flush=True) # noqa: T201
4849

4950

5051
if __name__ == "__main__":

src/async_utils/_simple_lock.py

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,18 @@
2121

2222
# TODO: pick what public namespace to re-export this from.
2323

24+
# This is one of the few things that should unreservedly be reimplemented natively.
25+
# Atomic ops allow the design here to be lockfree.
26+
# Actually using a futex here might improve performance further, though I doubt it.
27+
28+
# We can also do a little bit better prior to making it native by only locking
29+
# if we observe multiple threads. The gain for this is minor, but not insignificant.
30+
31+
# This particular lock implementation optimizes for the uncontested lock case.
32+
# This is the most important optimization for well-designed parallel code
33+
# with fine-grained lock use. It's also barging, which is known to be better
34+
# than FIFO in the case of highly contested locks.
35+
2436

2537
class AsyncLock:
2638
"""An async lock that doesn't bind to an event loop."""
@@ -36,36 +48,26 @@ def __init__(self) -> None:
3648
self._lockv: bool = False
3749
self._internal_lock: threading.RLock = threading.RLock()
3850

39-
def __locked(self) -> bool:
40-
with self._internal_lock:
41-
return self._lockv or (any(not w.cancelled() for w in (self._waiters)))
42-
4351
async def __aenter__(self) -> None:
44-
# placing the body of acquire here
45-
# causes problems with eager tasks factories.
46-
await self.__acquire()
47-
48-
async def __acquire(self) -> None:
4952
with self._internal_lock:
50-
if not self.__locked():
53+
if not self._lockv:
5154
self._lockv = True
5255
return
5356

5457
fut: cf.Future[None] = cf.Future()
5558

56-
with self._internal_lock:
57-
self._waiters.append(fut)
59+
self._waiters.append(fut)
5860

5961
try:
6062
await asyncio.wrap_future(fut)
6163
except asyncio.CancelledError:
62-
if fut.done() and not fut.cancelled():
63-
self._lockv = False
64+
with self._internal_lock:
65+
if not self._lockv:
66+
self._maybe_wake()
6467
raise
6568

66-
finally:
67-
self._maybe_wake()
68-
return
69+
with self._internal_lock:
70+
self._lockv = True
6971

7072
def _maybe_wake(self) -> None:
7173
with self._internal_lock:

0 commit comments

Comments
 (0)