-
-
Notifications
You must be signed in to change notification settings - Fork 31.9k
asyncio.sleep may sleep less time then it should #75720
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Originally faced here: Simple code to reproduce: import asyncio
import time
async def main():
while True:
asyncio.ensure_future(asyncio.sleep(1))
t0 = time.time()
await asyncio.sleep(0.1)
t1 = time.time()
print(t1 - t0)
if t1 - t0 < 0.1:
print('bug ^')
break
loop = asyncio.get_event_loop()
loop.run_until_complete(main()) I thought that it may be related to event loop's internal clock, but changing time.time() with asyncio.get_event_loop().time() doesn't help. Tested on Windows 10 x64, Python 3.6.2. |
I can't reproduce this on a linux VM with 3.7 tip. I don't currently have a windows instance to test against. |
AFAIR the Windows clock has a 15ms granularity, so I'm not really surprised. In other words, I don't think What happens if you replace |
Clocks is a very complex topic, especially when you care of portability. See my PEP-418. asyncio uses time.monotonic() clock to not be impacted when the system clock ("wall clock") is updated by the administrator or automatically by NTP. On Windows, time.monotonic() is implemented with GetTickCount64() which has a bad resolution: around 15 ms. The exact resolution depends on the hardware. See my PEP-418 which added time.monotonic() for Python 3.3 for much more information: To get the clock resolution, use:
Example on Linux: >>> import time; print(time.get_clock_info('monotonic'))
namespace(adjustable=False, implementation='clock_gettime(CLOCK_MONOTONIC)', monotonic=True, resolution=1e-09) Well, the announced resolution is 1 nanosecond, but in practice it's more around 500 ns. At least, it's better than 15 ms :-) -- asyncio rounds timings using the clock resolution, time.get_clock_info('monotonic').resolution. Search for self._clock_resolution in Lib/asyncio/base_events.py. asyncio is not designed for real time. |
STINNER Victor, thanks for explanation! I don't know if this issue has practical disadvantages, but such behavior seems to be confusing, especially, since it can be reproduced with event_loop.time() and asyncio doc says nothing about resolution - Do you think this issue should be closed? |
Technically, asyncio cannot wait exactly 100 ms on Windows. It will be more |
I don't think the bug is about real time here, rather about minimal sleep time expectations; i.e. it's expected that the sleep time may be *more* than asked for, but not less than. (and I would share that expectation myself) |
Exactly, and asyncio tries to ensure that. It looks like "time.get_clock_info('monotonic').resolution" lies about monotonic clock resolution on Windows so that might be the actual issue we want to address. |
IMHO we should either just close the issue, ir document the bad clock Yury: asyncio can trigger events earlier than scheduled for performance |
Le 21/09/2017 à 23:31, STINNER Victor a écrit :
If that's true, then it sounds like a bug. If there is a rationale for |
So here's the relevant piece of code: 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) 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. |
Issues: The performance issue was that the asyncio core loop was running in a loop Hopefully, select and selectors now round towards +inf (rather than |
Hm. I glanced through the issues you linked, and it looks like it's a valid concern for a micro-benchmark. But I don't see how a performance of a real-world application can be affected here. IOW, the "self.time() + self._clock_resolution" line looks like an optimization for a particular micro-benchmark. I'm sure we can construct another micro-benchmark that would benefit from "self.time() - self._clock_resolution". I think we should value correctness more than micro-benchmarks. |
When the fine tuning options for install-directories are set, the default directories "lib", "bin" and "include" are still created with essential content. Even if options like --libdir are set. |
Markus, your comment does not seem relevant to this issue. Maybe you meant to add it to a different issue, or you meant to create a new issue? |
On Windows >>> time.get_clock_info('monotonic')
namespace(implementation='GetTickCount64()', monotonic=True, adjustable=False, resolution=0.015625)
>>> time.get_clock_info('perf_counter')
namespace(implementation='QueryPerformanceCounter()', monotonic=True, adjustable=False, resolution=1e-07) I suppose one possible fix is to use |
But should we do that? Or can we provide a recipe to users? IIUC there's a way to change the clock used by asyncio. |
It is possible by subclassing but it doesn't work out of box with |
Okay, go for it. But let's treat it as a new feature. |
PR #97027 uses |
If someone's looking to pursue this, zooba left recommendations here: #97027 (comment) |
@kumaraditya303 What are your thoughts on possibly closing this issue? I'm not seeing a clear path forward with this issue. |
I close the issue. It's fixed in Python 3.13 thanks to clocks with better resolution on Windows!
This issue was reported on Windows with Python 3.6. Python 3.6 uses GetTickCount64() clock with a resolution of 15.6 ms for the asyncio loop, and GetSystemTimeAsFileTime() for time.time(). The reproducer uses the two clocks which may not be synchronized. What changes is that clocks on Windows on Python 3.13 have a way better resolution: https://docs.python.org/dev/whatsnew/3.13.html#time
asyncio uses time.monotonic(). So even if there is still a rounding issue in asyncio, the delta should now be lower than 1 µs and so it should matter way less :-)
I agree that this issue can be closed. I can no longer reproduce the issue on Python 3.6 using the reproducer. I cannot reproduce the "bug ^" neither with my modified reproducer: import asyncio
import time
async def main():
while True:
print(time.time())
task = asyncio.ensure_future(asyncio.sleep(1))
t0 = time.time()
await asyncio.sleep(0.1)
t1 = time.time()
print(t1 - t0, f"diff {(t1 - t0 - 0.1) * 1e3:+.1f} ms")
if t1 - t0 < 0.1:
print('bug ^')
break
await task
start = time.time()
try:
asyncio.run(main())
except KeyboardInterrupt:
print("run for %.1f sec" % (time.time() - start)) I ran this script for 30 seconds. Example of output:
The difference is now always positive. The error is between +8 and +10 ms, but at least it's no longer negative. Internally, asyncio event loop now uses a "clock resolution" of 100 ns (1e-07 sec). |
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
bugs.python.org fields:
The text was updated successfully, but these errors were encountered: