pythonpointerswhile-looppython-asyncio

Pointer in while loop condition in Python


I need function_B to wait for function_A. The wait_for_action_to_be_done() function does the job but I would like something more convenient with wait_for(action_to_wait). The issue is that, in wait_for(action_to_wait) the action_to_wait variable in the condition in the while loop is never updated. I guess something like a pointer would help.

import asyncio

action_done = False

async def function_A(delay):
    global action_done
    while True:
        await asyncio.sleep(delay)
        action_done = True
        break

async def wait_for(action_to_wait):
    print("wait for action to be done...")
    while not action_to_wait:
        await asyncio.sleep(0.1)

async def wait_for_action_to_be_done():
    print("wait for action to be done...")
    while not action_done:
        await asyncio.sleep(0.1)

async def function_B():
    await wait_for_action_to_be_done() # OK, works
    # await wait_for(action_done) # KO, infinite while loop as the condition is never updated
    print("performing next action...")

async def main():
    await asyncio.gather(function_A(2), function_B())

asyncio.run(main())

Solution

  • async def wait_for(action_to_wait):
        print("wait for action to be done...")
        while not action_to_wait:
            await asyncio.sleep(0.1)
    

    while not action_to_wait: waits for a local variable to change, it will never change. once you bind the global to a local variable, the local variable doesn't change when the global one does, see Facts and myths about Python names and values.

    You need to wait for a non-local object to change, the easiest way is to use a lambda. (or nonlocal with an inline function).

    async def wait_for(action_to_wait):
        print("wait for action to be done...")
        while not action_to_wait():
            await asyncio.sleep(0.1)
    
    async def function_B():
        # await wait_for_action_to_be_done() # OK, works
        await wait_for(lambda: action_done)  # works!
        print("performing next action...")
    

    Lastly using await asyncio.sleep(0.1) is fundamentally wrong, you should be using asyncio.Event instead.

    import asyncio
    
    
    async def function_A(delay, wait_event: asyncio.Event):
        while True:
            await asyncio.sleep(delay)
            wait_event.set()
            break
    
    async def wait_for(wait_event: asyncio.Event):
        print("wait for action to be done...")
        await wait_event.wait()
    
    async def function_B(wait_event: asyncio.Event):
        await wait_event.wait()  # works
        await wait_for(wait_event)  # also works
        print("performing next action...")
    
    async def main():
        wait_event = asyncio.Event()
        await asyncio.gather(function_A(2, wait_event), function_B(wait_event))
    
    asyncio.run(main())
    

    The rule of thumb is that if you are using a sleep then you are likely doing something wrong, sleeps should only be used for inserting a real life delay or a timer, not waiting for things to happen.

    If you are waiting for an event to happen from another thread you should be using a mixture of threading.Event and await loop.run_in_executor to wait for it, as asyncio.Event can only be used from inside the eventloop (not thread-safe).


    note that you can simulate what you know about pointers in other languages using an object or a list or a dictionary or a dummy class.

    class PackedBool:
        def __init__(self, value: bool):
            self.value = value
    
    action_done = PackedBool(False)  # access using action_done.value
    

    this is largely unnecessary for a bool where we already have the far superior events.