Search code examples
python-3.xpython-asyncio

Are Python async corountines run synchronously?


I am trying to understand more about asyncio internals. From the documentation, it seems that async functions are program-pointed mechanisms to enable blocking parts of code to execute out-of-order so other nonblocking parts can continue running until it hits a user defined blocking part again.

  1. Would the following program be safe to run without locks?
  2. If a blocking gives up execution for a long-running non-blocking part, the event loop by itself never evicts it? Does eviction need to be user-defined?

shared_counter = 0
lock = asyncio.Lock()


async def increment():
    global shared_counter
    shared_counter += 1


async def main():
    tasks = [increment() for _ in range(10000)] 
    await asyncio.gather(*tasks)


if __name__ == "__main__":
    asyncio.run(main())
    print("Final counter value:", shared_counter)

Solution

  • When writing async code, you need to code to the principle not the implementation.

    Would the following program be safe to run without locks?

    No, the operation is non-atomic and a race condition. A task could read the variable, any other number of tasks in the meantime can read and write to the value, then the original task could update the variable. Protect the data.

    In practice if you using a single thread for async, you'll never see the race condition. If you're using multiple threads, you still will not see the race condition because cpython has the GIL which a thread would have to release between reading and writing the variable. That won't happen, but it is not guaranteed. For example you could use a different python implementation or interface c that releases the GIL.

    Does eviction need to be user-defined?

    I am taking eviction to mean, the os will switch context and let something else run. There is no guarantee. If your running a cpu bound task, then python offers no guarantee that it will be interrupted from time to time to check the async event loop. So start long running expensive tasks with that expectation. A user defined check could be appropriate. Or something similar to the answer I linked in the comment, https://stackoverflow.com/a/71971903/2067492 create a future that you can include in your asynchronous work flow.

    Consider you have two tasks that you've submitted asynchronously. Each tasks has actions A1, A2,...AN and B1, B2, ... BN.

    When thinking of real time execution you have to consider that any order is possible. eg.

    a1, a2, a3, ... aN b1, b2, ..., bN
    

    That is a common execution order. But it could be:

    a1, b1, a2, b2, a3, ... bN, aN.
    

    Or even:

    b1, b2, b3, ... bN, a1, a2, ... aN
    

    The thing is async gives you the tools to make sure these tasks execute how you want them. You can have a3 be an action that waits for b3 and then our ordering possibilities are greatly reduced.

    In your example shared_counter += 1 would be 3 actions. a1 is read, a2 is value + 1, and a3 is write.