support icon indicating copy to clipboard operation
support copied to clipboard

[Bug] await wait(0) does not yield (enough)

Open laurensvalk opened this issue 1 year ago • 4 comments

Describe the bug The following program does not run the second stack of program, i.e. the buttons won't operate the gripper.

image

To reproduce

from pybricks.hubs import InventorHub
from pybricks.parameters import Button, Direction, Port, Stop
from pybricks.pupdevices import Motor, Remote
from pybricks.tools import multitask, run_task, wait

# Set up all devices.
inventor_hub = InventorHub()
gripper = Motor(Port.E, Direction.CLOCKWISE)
remote = Remote(timeout=None)

async def main1():
    print('Hello, Pybricks!')
    while True:
        await wait(0) # < ---- automatically inserted, but not enough

async def main2():
    while True:
        await wait(0)
        if Button.LEFT in remote.buttons.pressed():
            await gripper.run_target(500, 80)
        elif Button.RIGHT in remote.buttons.pressed():
            await gripper.run_target(500, 0)
        else:
            await wait(50)


async def main():
    await multitask(main1(), main2())

run_task(main())

Expected behavior Bad loops are caught by the block language to automatically yield. This is working, but apparently await wait(0) does not currently appear to yield at least once when it should.

The workaround would be to insert wait(1) but it would be better to fix it in the firmware.

laurensvalk avatar Feb 08 '24 12:02 laurensvalk

So this program will not print anything.

from pybricks.tools import multitask, run_task, wait

async def main1():
    while True:
        await wait(0)

async def main2():
    while True:
        await wait(0)
        print('Hello!')
        await wait(500)


async def main():
    await multitask(main1(), main2())

run_task(main())

image

laurensvalk avatar Feb 19 '24 08:02 laurensvalk

We can simplify this and rule out the multitask function as the culprit because it still fails without it:

from pybricks.tools import run_task, wait

async def main1():
    while True:
        await wait(0) # second taks only proceeds with nonzero delay

async def main2():
    while True:
        await wait(0)
        print('Hello!')
        await wait(500)

async def main():
    
    first = main1()
    second = main2()

    while True:
        print("first")
        next(first)
        print("second") # never gets here, so main1 is blocking
        next(second)

run_task(main())

laurensvalk avatar Feb 19 '24 09:02 laurensvalk

So await wait(0) just doesn't yield.

So we could either

  1. Make it yield once
  2. Use await wait(1)

I kind of want to avoid 2 because it would mean simple math loops would unnecessarily take 1 ms around the loop.

But when looking at this, the loop time doesn't seem to work as intended which is a separate issue.

EDIT: Assuming this issue is fixed, it's going to be a 10ms roundtrip around the loop anyway, so maybe we should just use the 1 ms here.

laurensvalk avatar Feb 19 '24 09:02 laurensvalk

My gut reaction here is to make await wait(0) yield once (pretty sure this is what await asyncio.sleep(0) does) and do https://github.com/pybricks/support/issues/1460#issuecomment-1952075493.

dlech avatar Feb 19 '24 17:02 dlech

Fixes suggested in https://github.com/pybricks/pybricks-micropython/pull/262, https://github.com/pybricks/pybricks-micropython/pull/263.

While I was thinking about this, it occurred to me we could extend this by making all awaitables yield at least once. For example await sensor.reflection() now only yields if there is a mode change to be done. By making it always yield once, any loop with at least one await statement would be safe. But since Python doesn't do this, I think it is probably not good to teach this pattern, and so we shouldn't do this. And it would slow down all those statements by one extra iteration. End of mental note.

laurensvalk avatar Aug 26 '24 15:08 laurensvalk