Skip to content

avoid the global_lock in asyncio.mixins._LoopBoundMixin #95474

Closed as not planned
@graingert

Description

@graingert

@asvetlov

The alternative is using the fast C atomic compare_and_swap function
which is executed under the hold GIL.
We need the pure-Python fallback anyway.

I was checking some minor detail in socketpair and noticed this:

vars(self).setdefault('_threads', _Threads())

and it looks like it benchmarks a 100ns faster than a global lock:

#!/usr/bin/env python3
import pyperf
from asyncio import events

"""Event loop mixins."""

import threading

_global_lock = threading.Lock()

# Used as a sentinel for loop parameter
_marker = object()


class _LoopBoundMixin:
    _loop = None

    def __init__(self, *, loop=_marker):
        if loop is not _marker:
            raise TypeError(
                f'As of 3.10, the *loop* parameter was removed from '
                f'{type(self).__name__}() since it is no longer necessary'
            )

    def _get_loop(self):
        loop = events._get_running_loop()

        if self._loop is None:
            with _global_lock:
                if self._loop is None:
                    self._loop = loop
        if loop is not self._loop:
            raise RuntimeError(f'{self!r} is bound to a different event loop')
        return loop


class _LoopBoundMixinSetDefault:
    _loop = None

    def __init__(self, *, loop=_marker):
        if loop is not _marker:
            raise TypeError(
                f'As of 3.10, the *loop* parameter was removed from '
                f'{type(self).__name__}() since it is no longer necessary'
            )

    def _get_loop(self):
        loop = events._get_running_loop()
        if self._loop is None:
            self.__dict__.setdefault("_loop", loop)

        if self._loop is not loop:
            raise RuntimeError(f'{self!r} is bound to a different event loop')

        return loop

runner = pyperf.Runner()
runner.timeit(name="get loop with lock",
              stmt="lbm = _LoopBoundMixin(); lbm._get_loop(); lbm._get_loop()",
              setup="import asyncio; from __main__ import _LoopBoundMixin; asyncio._set_running_loop(asyncio.new_event_loop())")

runner.timeit(name="get loop with setdefault",
              stmt="lbm = _LoopBoundMixin(); lbm._get_loop(); lbm._get_loop()",
              setup="import asyncio; from __main__ import _LoopBoundMixinSetDefault as _LoopBoundMixin; asyncio._set_running_loop(asyncio.new_event_loop())")

runner.timeit(name="get loop already set with lock",
              stmt="lbm._get_loop()",
              setup="import asyncio; from __main__ import _LoopBoundMixin; asyncio._set_running_loop(asyncio.new_event_loop()); lbm = _LoopBoundMixin(); lbm._get_loop()")

runner.timeit(name="get loop already set with setdefault",
              stmt="lbm._get_loop()",
              setup="import asyncio; from __main__ import _LoopBoundMixinSetDefault as _LoopBoundMixin; asyncio._set_running_loop(asyncio.new_event_loop()); lbm = _LoopBoundMixin(); lbm._get_loop()")

Originally posted by @graingert in #86558 (comment)

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.10only security fixes3.11only security fixes3.12only security fixestopic-asyncio

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions