classification
Title: asyncio.sleep(0) should "yield" back to the event loop, but it doesn't behave as expected
Type: behavior Stage: resolved
Components: asyncio Versions: Python 3.7
process
Status: closed Resolution: not a bug
Dependencies: Superseder:
Assigned To: Nosy List: Davy Durham, asvetlov, bar.harel, yselivanov
Priority: normal Keywords:

Created on 2020-05-28 05:36 by Davy Durham, last changed 2020-12-01 17:50 by asvetlov. This issue is now closed.

Messages (3)
msg370164 - (view) Author: Davy Durham (Davy Durham) Date: 2020-05-28 05:36
I was searching for a way to "yield" from task/coroutinue back to the event loop (not yielding a value in terms of a generator) and not finding anything documented, I found this bug report and PR: 
   https://github.com/python/asyncio/issues/284

It states that asyncio.sleep(0) should cause the coroutine to send control back to the event loop without wastefully doing other work.  That makes sense and a perfectly good way to do that.

And, the code appears to handle a value of <= 0 specifically: https://github.com/python/cpython/blob/3.8/Lib/asyncio/tasks.py#L632

However, using sleep(0) to yield back does not cause it to raise a CancelledError if the task has been cancelled as cancel()'s documentation indicates it should.  But sleeping for anything >0 does (e.g. 0.001)

The below code snippet will demonstrate the problem:

TIA

----

import asyncio
import time

async def cancel_me():
    print('cancel_me(): before sleep')

    try:
        while True:
            print("doing some really intensive cpu stuff")
            time.sleep(2)

            # now I want to yield control back to the event loop in order to determine if we've been cancelled 
            await asyncio.sleep(0) # I'm expecting this to throw CancelledError, but it never does.. it DOES throw if the delay > 0 (e.g. 0.001)

    except asyncio.CancelledError:
        print('cancel_me(): cancelled!')
        raise



async def main():
    task = asyncio.create_task(cancel_me())

    await asyncio.sleep(1)

    task.cancel()
    try:
        await task
    except asyncio.CancelledError:
        print("main(): cancel_me is cancelled now")

asyncio.run(main())
msg382271 - (view) Author: Bar Harel (bar.harel) * Date: 2020-12-01 17:47
It relinquishes control for exactly one event loop cycle.

You'll see that it takes two or three cycles and then gets cancelled correctly.

The reason being is that recovery from your `await asyncio.sleep(1)` takes two or three cycles. (Under the hood, a few `asyncio.call_soon()` are called in a chain and only 1 is executed each cycle).

This is not a bug, but rather intended behavior. You should probably use `asyncio.to_thread()` or preferably run cpu-intensive code on a different process.

@triage Please close as not-a-bug.
msg382272 - (view) Author: Andrew Svetlov (asvetlov) * (Python committer) Date: 2020-12-01 17:50
Agree, not a bug.
History
Date User Action Args
2020-12-01 17:50:38asvetlovsetstatus: open -> closed
resolution: not a bug
messages: + msg382272

stage: resolved
2020-12-01 17:47:12bar.harelsetnosy: + bar.harel
messages: + msg382271
2020-05-28 05:36:59Davy Durhamcreate