pythonasynchronousconcurrencythread-safetypython-asyncio

Async changing of global variables safe ? (Python)


I've been researching when I work in ASGI environments (single-threaded) whether it is safe to mutate singletons or global variables. I thought initially that it would be safe, but AI tells me it's not safe and I'm having a tough time believing it because I cannot reproduce it. Look at this code here.

import asyncio
import random

counter = 0

async def increment(name: str):
    global counter
    await asyncio.sleep(random.uniform(0, 5))  # yield control to event loop
    counter = counter + 1      # write (based on possibly stale value)
    print(f"{name}: counter → {counter}")

async def main():
    await asyncio.gather(*[increment(f"{i}") for i in range(10000)])
    print(f"Final counter (should be 10000): {counter}")

if __name__ == "__main__":
    asyncio.run(main())

This retuns correctly at the end. Here is an example of the last lines of input:

7825: counter → 9995
8403: counter → 9996
6039: counter → 9997
5887: counter → 9998
9942: counter → 9999
8631: counter → 10000
Final counter (should be 10000): 10000

This is safe right ?


Solution

  • The interesting statement is this one:

    counter = counter + 1
    
    

    On general principles this not thread-safe. Thread switches are controlled by the operating system and can occur at any time. If a thread switch happens just after reading the current value of counter, before writing the new value, this statement is a potential bug.

    However, your example program is not multi-threaded. Asyncio Task switches are not under control of the system and can only occur at await expressions. In a single-threaded asyncio program, the statement above is not a potential bug; the design of asyncio guarantees that it will work, as your results prove.

    Threading is pre-emptive. Asyncio is cooperative. The two approaches have different strengths and weaknesses.