[Python] When is `async for` faster than `for` (python)?

Discussão em 'Python' iniciado por Stack, Março 3, 2025.

  1. Stack

    Stack Membro Participativo

    In python, one can replace for [...] in [generator] by async for [...] in [awaitable generator]. Clearly, this lowers the overall execution time of a program if the [generator] contains some slow I/O operation that can be turned into a coroutine. I give a detailed example of this case below, along with analytic expressions for the execution times in various cases.

    My question

    Is this the only case where using async for instead of for lowers the overall execution time of a program?

    Or can async for be used for other purposes as well? If yes, please give practical examples that illustrate how async for makes smth run faster than with for.

    Example code

    • Make sure to call TEST_ASYNC_FOR() in the correct way; see comments at end of code.
    • Feel free to change BLK, NBLK, GEN and N to play around.

    async def TEST_ASYNC_FOR():

    import asyncio
    import time

    BLK = 0.15 # appears as `time.sleep(BLK)` inside both for-loops
    NBLK = 0.16 # appears as `await asyncio.sleep(NBLK)` inside both for-loops
    GEN = 0.17 # appears as `time.sleep(GEN)` (`await asyncio.sleep(GEN)`) inside the standard (async) generator
    N = 3 # number of iterations in each for-loop

    def generator():
    for i in range(N):
    time.sleep(GEN) # slow I/O operation blocks thread
    yield i

    async def async_generator():
    for i in range(N):
    await asyncio.sleep(GEN) # slow I/O operation yields control to loop
    yield i

    async def standard_for():
    for i in generator():
    time.sleep(BLK) # CPU-intensive operation blocks thread
    await asyncio.sleep(NBLK) # slow I/O operation yields control to loop

    async def async_for():
    async for i in async_generator():
    time.sleep(BLK) # CPU-intensive operation blocks thread
    await asyncio.sleep(NBLK) # slow I/O operation yields control to loop

    t0 = time.perf_counter()
    await standard_for()
    print(f"1x for-loop:\t\t\t {time.perf_counter()-t0:.2f} | expected: {N*BLK + N*NBLK + N*GEN:.2f} = N*BLK + N*NBLK + N*GEN")

    t0 = time.perf_counter()
    await async_for()
    print(f"1x async for-loop:\t\t {time.perf_counter()-t0:.2f} | expected: {N*BLK + N*NBLK + N*GEN:.2f} = N*BLK + N*NBLK + N*GEN")

    def standard_for_duration(BLK, NBLK, GEN, N):
    t = 2*N*BLK + 2*N*GEN + NBLK + (N-1)*max(NBLK-(BLK+GEN), 0)
    return t, "2*N*BLK + 2*N*GEN + NBLK + (N-1)*max(NBLK-(BLK+GEN), 0)"

    t0 = time.perf_counter()
    await asyncio.gather(standard_for(), standard_for())
    t_st, expr_st = standard_for_duration(BLK, NBLK, GEN, N)
    print(f"2x for-loop (separate tasks):\t {time.perf_counter()-t0:.2f} | expected: {t_st:.2f} = {expr_st}")

    def async_for_duration(BLK, NBLK, GEN, N):
    t = 2*N*BLK + N*GEN + NBLK + (N-1)*max(NBLK-BLK, 0) + (N-1)*GEN*(BLK > NBLK)*(NBLK > GEN) + GEN*(NBLK > BLK)*(BLK > GEN)
    return t, "2*N*BLK + N*GEN + NBLK + (N-1)*max(NBLK-BLK, 0) + (N-1)*GEN*(BLK > NBLK)*(NBLK > GEN) + GEN*(NBLK > BLK)*(BLK > GEN)"

    t0 = time.perf_counter()
    await asyncio.gather(async_for(), async_for())
    t_async, expr_async = async_for_duration(BLK, NBLK, GEN, N)
    print(f"2x async for-loop (separate tasks): {time.perf_counter()-t0:.2f} | expected: {t_async:.2f} = {expr_async}")

    await TEST_ASYNC_FOR() # use inside jupyter lab, which already has an event loop running

    # import asyncio
    # asyncio.run(TEST_ASYNC_FOR()) # use in a standalone python script


    The code outputs:

    1x for-loop: 1.44 | expected: 1.44 = N*BLK + N*NBLK + N*GEN
    1x async for-loop: 1.44 | expected: 1.44 = N*BLK + N*NBLK + N*GEN
    2x for-loop (separate tasks): 2.08 | expected: 2.08 = 2*N*BLK + 2*N*GEN + NBLK + (N-1)*max(NBLK-(BLK+GEN), 0)
    2x async for-loop (separate tasks): 1.59 | expected: 1.59 = 2*N*BLK + N*GEN + NBLK + (N-1)*max(NBLK-BLK, 0) + (N-1)*GEN*(BLK > NBLK)*(NBLK > GEN) + GEN*(NBLK > BLK)*(BLK > GEN)

    Continue reading...

Compartilhe esta Página