-
-
Notifications
You must be signed in to change notification settings - Fork 30.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[document removal of] the deprecated 'loop' parameter asyncio API in 3.10 #86558
Comments
asyncio.Lock and other primitives should no longer accept the |
(or alternatively they can cache the running |
Kyle, lmk if you want to work on this. |
This was on my to-do list but I very much appreciate if somebody champions this issue. |
Sure, I would be interested in helping with this. Although if a newer contributor takes it up before I'm able to, I wouldn't be opposed to that either (my main priority at the moment is helping with PEP-594 since it's a concrete goal of my internship w/ the PSF, but I should have some time to work on this as well). As far as I can tell though, there's currently a similar PR open: #18195 . This attempts to deprecate the loop argument and creating primitives such as asyncio.Lock outside of a running event loop with the following approach:
So, do we want to add more strictness to that with always using
AFAIK, at the C-API extension level, get_running_loop() already caches the running loop in cpython/Modules/_asynciomodule.c Line 232 in 9c98e8c
get_running_loop() to access the same event loop? I think it also adds a nice integrity check to be certain that the primitive wasn't initially created within a running event loop, and then later accessed outside of one.
The only concern that I can see with this approach is that users could potentially create a primitive in one running event loop and then access it in a separate loop running in a different thread (without using something like self._loop, the primitive would no longer be associated with a specific event loop and could potentially used within *any* running event loop). I'm not entirely certain if that is a real problem though, and if anything, it seems like it could prove to be useful in some multi-loop environment. I just want to make sure that it's intended. |
Oh my. FWIW I think that we need to implement this differently. I don't think it matters where, say, an asyncio.Lock was instantiated. It can be created anywhere. So IMO its init shouldn't try to capture the current loop -- there's no need for that. The loop can be and should be captured when the first I'm writing a piece of code right now that would need to jump through the hoops to simply create a new |
That's good to know and I think more convenient to work with, so +1 from me. I guess my remaining question though is whether it's okay to
From what I've seen of asyncio user code, it seems reasonably common to create async primitives (lock, semaphore, queue, etc.) in the init for some class prior to using the event loop, which would fail with usage of |
No, it's not OK to use one lock across multiple loops at the same time. But most asyncio code out there doesn't have protections against that, and we never advertised that having multiple loops run in parallel is a good idea. So while we could build protections against that, I'm not sure its needed. Andrew, thoughts?
Yep. This sums up how I think of this now. |
My initial thought was protecting the Lock (and other primitives) creation when a loop is not running. Yuri insists that Lock can be created without a loop. Technically it is possible, sure. Thus, the global scoped lock object still looks suspicious in my eyes. Another thing to consider is: whether to cache a loop inside a lock; whether to add a check when the lock is used by two loops? I think for the last question the answer is "yes". I recall many questions and bug reports on StackOverflow and aiohttp bug tracker when people use the multithreaded model for some reason and tries to get access to a shared object from different threads that executes each own loop. The check becomes extremely important for synchronization primitives like asyncio.Lock class; threading.Lock is supposed to be shared between threads and users can apply the same pattern for asyncio.Lock by oversight. |
Yeah, I follow the reasoning. My use case: class Something:
def __init__(self):
self._lock = asyncio.Lock()
async def do_something():
async with self._lock:
... And
I agree. Maybe the solution should be this then:
def _get_loop(self):
loop = asyncio.get_running_loop()
if self._loop is None:
self._loop = loop
if loop is not self._loop: raise
if not loop.is_running(): raise
async def acquire(self):
loop = self._get_loop()
... This is what would guarantee all protections you want and would also allow to instantiate |
Despite the fact that asyncio.get_running_loop() never returns None but raises RuntimeError (and maybe other tiny cleanups), It doesn't make a system worse at least and backward compatible. P.S. |
It never raises when called from inside a coroutine. And I propose to do that. So it will never fail. |
Regarding the example _get_loop():
Would this be added to all asyncio primitives to be called anytime a reference to the loop is needed within a coroutine? Also, regarding the last line "if not loop.is_running(): raise" I'm not 100% certain that I understand the purpose. Wouldn't it already raise a RuntimeError from The only thing I can think of where it would have an effect is if somehow the event loop was running at the start of
(Unless you intended for the first line Other than that, I think the approach seems solid. |
Perhaps Kyle is right, I had a misunderstanding with The last version seems good except for the rare chance of race condition. The safe code can look like: global_lock = threading.Lock() like GIL
def _get_loop(self):
loop = asyncio.get_running_loop()
if self._loop is None:
# the lock is required because
# the thread switch can happen
# between `self._loop is None` check
# and `self._loop = loop` assignment
with global_lock:
if self._loop is not None:
self._loop = loop
if loop is not self._loop: raise The alternative is using the fast C atomic Multithreading is hard... |
SGTM. We can use this strategy for all synchronization primitives and for objects like asyncio.Queue. |
Perfect! We have a consensus now and waiting for a champion who propose a Pull Request. |
Sorry, there are a few things in the committed patch that should be fixed. See the PR for my comments. |
Is there anyone who is assigned to removing the deprecated If not I can take this task, I believe I have enough free time and curiosity to do that :) |
You can certainly feel free to work on that and it would definitely be appreciated! However, I would recommend splitting it into several PRs, basically as "Remove *loop* parameter from x` rather than doing a massive PR that removes it from everywhere that it was deprecated. This makes the review process easier. Also, keep in mind that there are some uses of loop that are still perfectly valid and will remain so, such as in |
Looks like we have done everything, we can close this issue |
Thanks for your help! |
There appear to be no versionchanged:: 3.10 in the asyncio docs on the APIs that formerly accepted a loop= parameter linking people to information on when that went away (3.10) and why. Specifically I'm talking about https://docs.python.org/3.10/library/asyncio-stream.html. The asyncio stack traces people will face when porting code to 3.10 are mystifying (they may not even show use of a loop parameter) when this comes up, so we should really leave more bread crumbs than expecting people to find the What's New doc.
Arguably something similar to that whatsnew text should've been added to the docs in 3.8 with the loop deprecation. Something like this?
including a ReST link to more info in the whats new doc on the last entry would be useful. |
Sorry, I missed that. Working on it. |
thanks! |
I was checking some minor detail in socketpair and noticed this: Line 700 in bceb197
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()") |
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
bugs.python.org fields:
The text was updated successfully, but these errors were encountered: