Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
374 views
in Technique[技术] by (71.8m points)

python - Is `asyncio.sleep(delay)` guarenteed to sleep for at least `delay` seconds?

asyncio.sleep()'s blocking cousin, time.sleep(), cannot guarantee that it will sleep for the requested amount of time.

The actual suspension time may be less than that requested because any caught signal will terminate the sleep() following execution of that signal’s catching routine.

asyncio.sleep()'s documentation does not mention a similar limitation.

Is asyncio.sleep() able to make stronger guarantees about the amount of time it will sleep for?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

I will not tell what asyncio guarantees, but based on the implementation it follows that the asyncio.sleep()(basically, call_later()) sleeps for the specified interval, but at least with an inaccuracy equal to the resolution of the system clock used in the implementation.

Let's figure it out. First, asyncio uses monotonic clocks, which have different resolutions on different platforms (both Python and OS resolutions). For example, for Windows this is as much as 15ms.

In terms of guarantees, pay attention to the comment to the function BaseEventLoop.time:

    def time(self):
        """Return the time according to the event loop's clock.
        This is a float expressed in seconds since an epoch, but the
        epoch, precision, accuracy and drift are unspecified and may
        differ per event loop.
        """
        return time.monotonic()

Now let's take a look at the asyncio event loop source code responsible for starting the scheduled timers:

        # Handle 'later' callbacks that are ready.
        end_time = self.time() + self._clock_resolution
        while self._scheduled:
            handle = self._scheduled[0]
            if handle._when >= end_time:
                break
            handle = heapq.heappop(self._scheduled)
            handle._scheduled = False
            self._ready.append(handle)

Line end_time = self.time() + self._clock_resolution shows that the callback may fire earlier than planned but within the clock resolution. Yuri Selivanov clearly stated about this here:

As I see it, currently we peek into the future time. Why don't we do

       end_time = self.time() - self._clock_resolution

to guarantee that timeouts will always be triggered after the requested time, not before? I don't see how the performance can become worse if we do this.

And really, let's run the next program (Python 3.8 on Windows 10):

import asyncio 
import time

async def main():
    print("Timer resolution", time.get_clock_info('monotonic').resolution)
    while True:
        asyncio.create_task(asyncio.sleep(1))

        t0 = time.monotonic()
        await asyncio.sleep(0.1)
        t1 = time.monotonic()

        print(t1 - t0)
asyncio.run(main()) 

We see behaviour described above:

Timer resolution 0.015625
0.09299999987706542
0.0940000000409782
0.0940000000409782
0.10900000017136335
...

But at the beginning of the text, I said at least the clock resolution, because asyncio works in conditions of cooperative multitasking, and if there is a greedy coroutine (or many less greedy ones) that does not give control to the event loop too often, we have the following picture:

import asyncio 
import time

async def calc():
    while True:
        k = 0
        for i in range(1*10**6):        
            k += i
        await asyncio.sleep(0.1)  # yield to event loop
    

async def main():
    asyncio.create_task(calc())  # start greedy coroutine
    print("Timer resolution", time.get_clock_info('monotonic').resolution)
    while True:
        asyncio.create_task(asyncio.sleep(1))

        t0 = time.monotonic()
        await asyncio.sleep(0.1)
        t1 = time.monotonic()

        print(t1 - t0)
asyncio.run(main()) 

The situation is unsurprisingly changing towards increasing latency:

0.17200000025331974
0.1559999999590218
0.14100000029429793
0.2190000000409782

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...