Python async generator function without time drift

I am trying to emulated the generation of a sequence of numbers as shown in the code below. I want to execute this at regular intervals without time drift and without keeping track of the number of times serial_sequence() has been called, i.e. without using the variable num. How can this be done?

In the example code, delay2 is always 0.1, but it should be slightly less since the event loop is busy elsewhere between calls to serial_sequence(). Thus the elapsed time is over 2.0 seconds.

import  asyncio
import time
rx_data = list()
EOT = 20

async def serial_sequence():
    '''async generator function that simulates incoming sequence of serial data'''
    num = 0
    tprev = time.time()

    while True:
        dt = time.time() - tprev
        delay2 = 0.1 - dt #Want to avoid time drift
        print('dt: {}'.format(dt))
        print('delay2: {}'.format(delay2))
        await asyncio.sleep(delay2) #simulated IO delay
        tprev = time.time()
        num += 1
        yield  num

async def read_serial1():
    gen = serial_sequence()
    while(True):
        data = await gen.__anext__()
        rx_data.append(data)
        print('read_serial1:', data)
        if data == EOT:
            break
    return rx_data

async def main():
    start = time.time()
    task1 = asyncio.create_task(read_serial1())
    await(task1)
    stop = time.time()
    print('Elapsed: {}'.format(stop-start))

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

Answer

The code in the while loop itself needs some time to compute. The timedrift in your example accumulates because you base dt on the time of the previous timestep tprev.

You could instead use the absolute starting time of serial_sequence as point of reference like so:

async def serial_sequence():
    '''async generator function that simulates incoming sequence of serial data'''
    num = 0
    starttime = time.time()

    while True:
        dt = (time.time() - starttime) % 0.1
        delay2 = 0.1 - dt #Want to avoid time drift
        print('dt: {}'.format(dt))
        print('delay2: {}'.format(delay2))
        await asyncio.sleep(delay2) #simulated IO delay
        tprev = time.time()
        num += 1
        yield num

Compare the accumulation by changing EOT. I.e. Doubling EOT results in double the time drift for your solution, while it is approximately constant for this one.

My answer is largely based on this answer to a very similar question.