Skip to content

Commit 0c8ad51

Browse files
authored
Delegated the implementations of Lock and Semaphore to the async backend class (#761)
Also added the `fast_acquire` parameter for Lock and Semaphore.
1 parent ee8165b commit 0c8ad51

File tree

7 files changed

+618
-94
lines changed

7 files changed

+618
-94
lines changed

docs/synchronization.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@ Example::
6666

6767
run(main)
6868

69+
.. tip:: If the performance of semaphores is critical for you, you could pass
70+
``fast_acquire=True`` to :class:`Semaphore`. This has the effect of skipping the
71+
:func:`~.lowlevel.cancel_shielded_checkpoint` call in :meth:`Semaphore.acquire` if
72+
there is no contention (acquisition succeeds immediately). This could, in some cases,
73+
lead to the task never yielding control back to to the event loop if you use the
74+
semaphore in a loop that does not have other yield points.
75+
6976
Locks
7077
-----
7178

@@ -92,6 +99,12 @@ Example::
9299

93100
run(main)
94101

102+
.. tip:: If the performance of locks is critical for you, you could pass
103+
``fast_acquire=True`` to :class:`Lock`. This has the effect of skipping the
104+
:func:`~.lowlevel.cancel_shielded_checkpoint` call in :meth:`Lock.acquire` if there
105+
is no contention (acquisition succeeds immediately). This could, in some cases, lead
106+
to the task never yielding control back to to the event loop if use the lock in a
107+
loop that does not have other yield points.
95108

96109
Conditions
97110
----------

docs/versionhistory.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ This library adheres to `Semantic Versioning 2.0 <http://semver.org/>`_.
55

66
**UNRELEASED**
77

8+
- Improved the performance of ``anyio.Lock`` and ``anyio.Semaphore`` on asyncio (even up
9+
to 50 %)
10+
- Added the ``fast_acquire`` parameter to ``anyio.Lock`` and ``anyio.Semaphore`` to
11+
further boost performance at the expense of safety (``acquire()`` will not yield
12+
control back if there is no contention)
813
- Fixed ``__repr__()`` of ``MemoryObjectItemReceiver``, when ``item`` is not defined
914
(`#767 <https://github.com/agronholm/anyio/pulls/767>`_; PR by @Danipulok)
1015
- Added support for the ``from_uri()``, ``full_match()``, ``parser`` methods/properties

src/anyio/_backends/_asyncio.py

Lines changed: 178 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,13 @@
5858

5959
import sniffio
6060

61-
from .. import CapacityLimiterStatistics, EventStatistics, TaskInfo, abc
61+
from .. import (
62+
CapacityLimiterStatistics,
63+
EventStatistics,
64+
LockStatistics,
65+
TaskInfo,
66+
abc,
67+
)
6268
from .._core._eventloop import claim_worker_thread, threadlocals
6369
from .._core._exceptions import (
6470
BrokenResourceError,
@@ -70,9 +76,16 @@
7076
)
7177
from .._core._sockets import convert_ipv6_sockaddr
7278
from .._core._streams import create_memory_object_stream
73-
from .._core._synchronization import CapacityLimiter as BaseCapacityLimiter
79+
from .._core._synchronization import (
80+
CapacityLimiter as BaseCapacityLimiter,
81+
)
7482
from .._core._synchronization import Event as BaseEvent
75-
from .._core._synchronization import ResourceGuard
83+
from .._core._synchronization import Lock as BaseLock
84+
from .._core._synchronization import (
85+
ResourceGuard,
86+
SemaphoreStatistics,
87+
)
88+
from .._core._synchronization import Semaphore as BaseSemaphore
7689
from .._core._tasks import CancelScope as BaseCancelScope
7790
from ..abc import (
7891
AsyncBackend,
@@ -1658,6 +1671,154 @@ def statistics(self) -> EventStatistics:
16581671
return EventStatistics(len(self._event._waiters))
16591672

16601673

1674+
class Lock(BaseLock):
1675+
def __new__(cls, *, fast_acquire: bool = False) -> Lock:
1676+
return object.__new__(cls)
1677+
1678+
def __init__(self, *, fast_acquire: bool = False) -> None:
1679+
self._fast_acquire = fast_acquire
1680+
self._owner_task: asyncio.Task | None = None
1681+
self._waiters: deque[tuple[asyncio.Task, asyncio.Future]] = deque()
1682+
1683+
async def acquire(self) -> None:
1684+
if self._owner_task is None and not self._waiters:
1685+
await AsyncIOBackend.checkpoint_if_cancelled()
1686+
self._owner_task = current_task()
1687+
1688+
# Unless on the "fast path", yield control of the event loop so that other
1689+
# tasks can run too
1690+
if not self._fast_acquire:
1691+
try:
1692+
await AsyncIOBackend.cancel_shielded_checkpoint()
1693+
except CancelledError:
1694+
self.release()
1695+
raise
1696+
1697+
return
1698+
1699+
task = cast(asyncio.Task, current_task())
1700+
fut: asyncio.Future[None] = asyncio.Future()
1701+
item = task, fut
1702+
self._waiters.append(item)
1703+
try:
1704+
await fut
1705+
except CancelledError:
1706+
self._waiters.remove(item)
1707+
if self._owner_task is task:
1708+
self.release()
1709+
1710+
raise
1711+
1712+
self._waiters.remove(item)
1713+
1714+
def acquire_nowait(self) -> None:
1715+
if self._owner_task is None and not self._waiters:
1716+
self._owner_task = current_task()
1717+
return
1718+
1719+
raise WouldBlock
1720+
1721+
def locked(self) -> bool:
1722+
return self._owner_task is not None
1723+
1724+
def release(self) -> None:
1725+
if self._owner_task != current_task():
1726+
raise RuntimeError("The current task is not holding this lock")
1727+
1728+
for task, fut in self._waiters:
1729+
if not fut.cancelled():
1730+
self._owner_task = task
1731+
fut.set_result(None)
1732+
return
1733+
1734+
self._owner_task = None
1735+
1736+
def statistics(self) -> LockStatistics:
1737+
task_info = AsyncIOTaskInfo(self._owner_task) if self._owner_task else None
1738+
return LockStatistics(self.locked(), task_info, len(self._waiters))
1739+
1740+
1741+
class Semaphore(BaseSemaphore):
1742+
def __new__(
1743+
cls,
1744+
initial_value: int,
1745+
*,
1746+
max_value: int | None = None,
1747+
fast_acquire: bool = False,
1748+
) -> Semaphore:
1749+
return object.__new__(cls)
1750+
1751+
def __init__(
1752+
self,
1753+
initial_value: int,
1754+
*,
1755+
max_value: int | None = None,
1756+
fast_acquire: bool = False,
1757+
):
1758+
super().__init__(initial_value, max_value=max_value)
1759+
self._value = initial_value
1760+
self._max_value = max_value
1761+
self._fast_acquire = fast_acquire
1762+
self._waiters: deque[asyncio.Future[None]] = deque()
1763+
1764+
async def acquire(self) -> None:
1765+
if self._value > 0 and not self._waiters:
1766+
await AsyncIOBackend.checkpoint_if_cancelled()
1767+
self._value -= 1
1768+
1769+
# Unless on the "fast path", yield control of the event loop so that other
1770+
# tasks can run too
1771+
if not self._fast_acquire:
1772+
try:
1773+
await AsyncIOBackend.cancel_shielded_checkpoint()
1774+
except CancelledError:
1775+
self.release()
1776+
raise
1777+
1778+
return
1779+
1780+
fut: asyncio.Future[None] = asyncio.Future()
1781+
self._waiters.append(fut)
1782+
try:
1783+
await fut
1784+
except CancelledError:
1785+
try:
1786+
self._waiters.remove(fut)
1787+
except ValueError:
1788+
self.release()
1789+
1790+
raise
1791+
1792+
def acquire_nowait(self) -> None:
1793+
if self._value == 0:
1794+
raise WouldBlock
1795+
1796+
self._value -= 1
1797+
1798+
def release(self) -> None:
1799+
if self._max_value is not None and self._value == self._max_value:
1800+
raise ValueError("semaphore released too many times")
1801+
1802+
for fut in self._waiters:
1803+
if not fut.cancelled():
1804+
fut.set_result(None)
1805+
self._waiters.remove(fut)
1806+
return
1807+
1808+
self._value += 1
1809+
1810+
@property
1811+
def value(self) -> int:
1812+
return self._value
1813+
1814+
@property
1815+
def max_value(self) -> int | None:
1816+
return self._max_value
1817+
1818+
def statistics(self) -> SemaphoreStatistics:
1819+
return SemaphoreStatistics(len(self._waiters))
1820+
1821+
16611822
class CapacityLimiter(BaseCapacityLimiter):
16621823
_total_tokens: float = 0
16631824

@@ -2108,6 +2269,20 @@ def create_task_group(cls) -> abc.TaskGroup:
21082269
def create_event(cls) -> abc.Event:
21092270
return Event()
21102271

2272+
@classmethod
2273+
def create_lock(cls, *, fast_acquire: bool) -> abc.Lock:
2274+
return Lock(fast_acquire=fast_acquire)
2275+
2276+
@classmethod
2277+
def create_semaphore(
2278+
cls,
2279+
initial_value: int,
2280+
*,
2281+
max_value: int | None = None,
2282+
fast_acquire: bool = False,
2283+
) -> abc.Semaphore:
2284+
return Semaphore(initial_value, max_value=max_value, fast_acquire=fast_acquire)
2285+
21112286
@classmethod
21122287
def create_capacity_limiter(cls, total_tokens: float) -> abc.CapacityLimiter:
21132288
return CapacityLimiter(total_tokens)

0 commit comments

Comments
 (0)