-
-
Notifications
You must be signed in to change notification settings - Fork 595
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
Bug-prone background task creation suggestion in docs #647
Comments
Can you explain better what the problem is? If the coroutine is running, then there are references to it. At least one reference is held by asyncio itself, since it obviously knows about this coroutine and is able to schedule the CPU for it. If the error is still the "Task was destroyed but it is pending", that implies someone destroyed the task. I have asked you to provide the full error message including the stack trace back when you reported this, but you haven't. Typically this error occurs when the loop is destroyed before the task finished or was able to cancel. |
Hi, first, I forgot to highlight that
So this is actually a misleading part of asyncio. Asyncio actually maintains weak references to coroutines that are currently executing to prevent harder-to-debug memory leaks from reference cycles. So, this is why without a hard reference, Python's garbage collector can collect it at free will. You can read this thread from Python's bug tracker that covers asyncio's unusual behavior. The full error is generally as follows:
As you can see there is no stack trace at all as this is simply a message injected by some internal asyncio Task's A simple example of this is as follows: import asyncio
import weakref
future_refs = []
async def something():
future = asyncio.get_event_loop().create_future()
future_refs.append(weakref.ref(future))
await future
async def go():
asyncio.create_task(something())
await asyncio.sleep(0.5)
future_refs[0]().set_result(None)
asyncio.run(go()) Here, while it is a little strange that we only store weakrefs to futures, we would expect the event loop to still maintain a reference to the running coroutine
Note, we can force garbage collection at the end of every step in the event loop via the following:
Anyways, if we change that code to assign to some unused |
I'll look at this in more detail, but I don't understand the example that you posted. You created a future and only stored a weak reference to it. Then you said "if we force garbage collection at every step of the loop", which would get rid of the future. How is this supposed to work? Running your example and forcing a GC I get |
Hmm, are you sure that the modified Technically, I realize we could simplify the example to: import asyncio
async def something():
await asyncio.get_event_loop().create_future()
async def go():
asyncio.create_task(something())
await asyncio.sleep(0.5)
asyncio.run(go()) Despite the admittedly weird example, according to your assumption that "at least one reference is held by asyncio itself", we should not see "Task was destroyed but it is pending!" because asyncio is still executing the Just to make sure I explained it properly, near the bottom of the ntodo = len(self._ready)
for i in range(ntodo):
import gc; gc.collect() # <<<<<<<<<<<<<<<<<<<<<<<<<
handle = self._ready.popleft()
if handle._cancelled:
continue
if self._debug:
try:
self._current_handle = handle
t0 = self.time()
handle._run()
dt = self.time() - t0
if dt >= self.slow_callback_duration:
logger.warning('Executing %s took %.3f seconds',
_format_handle(handle), dt)
finally:
self._current_handle = None
else:
handle._run()
handle = None # Needed to break cycles when an exception occurs Anyways, I encourage you to read from the ticket from above if you get a chance to look into it in more detail. Specifically Guido mentions:
|
@MatthewScholefield Your simplified example is also contrived and not applicable to any real usage. Once again you are waiting on a future that is subject to garbage collection due to not having any references. If I replace the waiting on this future with a sleep, then the task is also pending, but it isn't destroyed. Do you understand this problem well enough to explain why is the sleep different than the weak future? |
Apologies, here's a real example: import asyncio
async def request_google():
reader, writer = await asyncio.open_connection('google.com', 80)
writer.write(b'GET / HTTP/2\n\n')
await writer.drain()
response = await reader.read()
return response.decode()
async def something():
data = await request_google()
print('Data:', data)
async def go():
asyncio.create_task(something())
await asyncio.sleep(10.0)
asyncio.run(go()) The reason async def sleep(delay, result=None, *, loop=None):
...
future = loop.create_future()
h = loop.call_later(delay,
futures._set_result_unless_cancelled,
future, result)
try:
return await future
finally:
h.cancel() Now, if we look at def call_at(self, when, callback, *args, context=None): # Note: call_later just calls call_at
...
timer = events.TimerHandle(when, callback, args, self, context)
...
heapq.heappush(self._scheduled, timer) So, this internal hard reference to a variable within the sleep coroutine keeps our original coroutine from being garbage collected. Let me know if this makes sense. |
The docs give the following example for asyncio:
This causes a number of subtle bugs around the coroutine running
my_background_task
getting garbage collected due to no references to it. Instead, perhaps the docs should be updated to something that warns of this like:If this looks alright, I'd be happy to do a PR.
The text was updated successfully, but these errors were encountered: