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
TEST_ASYNC_FOR()
in the correct way; see comments at end of code.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)