[英]Making a timer: timeout inaccuracy of threading.Event.wait - Python 3.6
首先,我是 Python 新手,不熟悉它的功能。 我一直主要使用MATLAB。
PC 簡要規格:Windows 10、Intel i7
我正在嘗試制作一個計時器類來定期執行 MATLAB 等函數,這顯然是從 Java 計時器借來的。 MATLAB 計時器的分辨率約為 1 毫秒,我從未見過它在任何情況下都超過 2 毫秒。 事實上,它對我的項目來說已經足夠准確了。
最近,由於MATLAB的並行計算和網絡訪問功能較差,我打算轉向Python。 然而,不幸的是,與我必須制作自己的計時器類的 MATLAB 相比,Python 的標准包提供了某種程度較低的計時器 (threading.Timer)。 首先,我提到了 QnA 在 Python 中執行周期性操作 [重復] 。 Michael Anderson提出的解決方案給出了漂移校正的簡單概念。 他使用 time.sleep() 來保持時間。 該方法非常准確,有時比 MATLAB 計時器顯示出更好的准確性。 大約 0.5 毫秒分辨率。 但是,定時器在time.sleep() 中被捕獲期間不能被中斷(暫停或恢復)。 但有時我必須立即停止,無論它是否處於 sleep() 狀態。
我發現的問題的解決方案是利用線程包中的 Event 類。 請參閱Python threading.timer - 每隔 'n' 秒重復一次函數。 使用 Event.wait() 的超時功能,我可以在執行之間設置時間間隔,並用於保持時間段。 也就是說,事件通常會被清除,因此 wait(timeout) 可以像 time.sleep(interval) 一樣,我可以在需要時通過設置 event 立即退出 wait()。
一切似乎都很好,但 Event.wait() 中存在一個嚴重問題。 時間延遲在 1 ~ 15 ms 之間變化太大。 我認為它來自 Event.wait() 的開銷。
我制作了一個示例代碼,顯示了 time.sleep() 和 Event.wait() 之間的准確性比較。 這總計 1000 次迭代 1 ms sleep() 和 wait() 以查看累積時間錯誤。 預期結果約為 1.000。
import time
from threading import Event
time.sleep(3) # to relax
# time.sleep()
tspan = 1
N = 1000
t1 = time.perf_counter()
for _ in range(N):
time.sleep(tspan/N)
t2 = time.perf_counter()
print(t2-t1)
time.sleep(3) # to relax
# Event.wait()
tspan = 1
event = Event()
t1 = time.perf_counter()
for _ in range(N):
event.wait(tspan/N)
t2 = time.perf_counter()
print(t2-t1)
結果:
1.1379848184879964
15.614547161211096
結果表明 time.sleep() 在准確性上要好得多。 但是我不能完全依賴前面提到的 time.sleep()。
總之,
我目前正在考慮一個折衷方案:就像在示例中一樣,創建一個微小的 time.sleep() 循環(間隔為 0.5 毫秒)並使用 if 語句退出循環並在需要時中斷。 據我所知,該方法用於 Python 2.x Python time.sleep() vs event.wait() 。
這是一個冗長的介紹,但我的問題可以總結如下。
我可以通過外部信號或事件強制線程進程從 time.sleep() 中斷嗎? (這似乎是最有效的。???)
使 Event.wait() 更准確或減少開銷時間。
除了 sleep() 和 Event.wait() 方法之外,是否還有更好的方法來提高計時精度。
非常感謝。
我遇到了與Event.wait()
相同的計時問題。 我想出的解決方案是創建一個模仿threading.Event
的類。 在內部,它結合使用time.sleep()
循環和 busy 循環來大大提高精度。 睡眠循環在單獨的線程中運行,因此主線程中的阻塞wait()
調用仍然可以立即中斷。 當set()
方法被調用時,睡眠線程應該在不久之后終止。 此外,為了最大限度地減少 CPU 使用率,我確保繁忙循環的運行時間不會超過 3 毫秒。
這是我的自定義Event
類以及最后的計時演示(演示中打印的執行時間將以納秒為單位):
import time
import _thread
import datetime
class Event:
__slots__ = (
"_flag", "_lock", "_nl",
"_pc", "_waiters"
)
_lock_type = _thread.LockType
_timedelta = datetime.timedelta
_perf_counter = time.perf_counter
_new_lock = _thread.allocate_lock
class _switch:
__slots__ = ("_on",)
def __call__(self, on: bool = None):
if on is None:
return self._on
self._on = on
def __bool__(self):
return self._on
def __init__(self):
self._on = False
def clear(self):
with self._lock:
self._flag(False)
def is_set(self) -> bool:
return self._flag()
def set(self):
with self._lock:
self._flag(True)
waiters = self._waiters
for waiter in waiters:
waiter.release()
waiters.clear()
def wait(
self,
timeout: float = None
) -> bool:
with self._lock:
return self._wait(self._pc(), timeout)
def _new_waiter(self) -> _lock_type:
waiter = self._nl()
waiter.acquire()
self._waiters.append(waiter)
return waiter
def _wait(
self,
start: float,
timeout: float,
td=_timedelta,
pc=_perf_counter,
end: _timedelta = None,
waiter: _lock_type = None,
new_thread=_thread.start_new_thread,
thread_delay=_timedelta(milliseconds=3)
) -> bool:
flag = self._flag
if flag:
return True
elif timeout is None:
waiter = self._new_waiter()
elif timeout <= 0:
return False
else:
delay = td(seconds=timeout)
end = td(seconds=start) + delay
if delay > thread_delay:
mark = end - thread_delay
waiter = self._new_waiter()
new_thread(
self._wait_thread,
(flag, mark, waiter)
)
lock = self._lock
lock.release()
try:
if waiter:
waiter.acquire()
if end:
while (
not flag and
td(seconds=pc()) < end
):
pass
finally:
lock.acquire()
if waiter and not flag:
self._waiters.remove(waiter)
return flag()
@staticmethod
def _wait_thread(
flag: _switch,
mark: _timedelta,
waiter: _lock_type,
td=_timedelta,
pc=_perf_counter,
sleep=time.sleep
):
while not flag and td(seconds=pc()) < mark:
sleep(0.001)
if waiter.locked():
waiter.release()
def __new__(cls):
_new_lock = cls._new_lock
_self = object.__new__(cls)
_self._waiters = []
_self._nl = _new_lock
_self._lock = _new_lock()
_self._flag = cls._switch()
_self._pc = cls._perf_counter
return _self
if __name__ == "__main__":
def test_wait_time():
wait_time = datetime.timedelta(microseconds=1)
wait_time = wait_time.total_seconds()
def test(
event=Event(),
delay=wait_time,
pc=time.perf_counter
):
pc1 = pc()
event.wait(delay)
pc2 = pc()
pc1, pc2 = [
int(nbr * 1000000000)
for nbr in (pc1, pc2)
]
return pc2 - pc1
lst = [
f"{i}.\t\t{test()}"
for i in range(1, 11)
]
print("\n".join(lst))
test_wait_time()
del test_wait_time
Chris D 的自定義 Event 類效果非常好! 出於實用目的,我將它包含在一個可安裝的包中( https://github.com/ovinc/oclock ,使用pip install oclock
),其中還包含其他計時工具。 從1.3.0版本oclock
及以后,可以使用自定義Event
的克里斯·D的答案,如討論班
from oclock import Event
event = Event()
event.wait(1)
使用Event
類的常用set()
、 clear()
、 is_set()
、 wait()
方法。
計時精度比使用threading.Event
好得多,尤其是在 Windows 中。 例如,在具有 1000 個重復循環的 Windows 機器上,我得到threading.Event
循環持續時間的標准偏差為 7ms, oclock.Event
小於 0.01 ms。 克里斯 D 的道具!
注: oclock
包下有StackOverflow上的CC BY-SA 4.0兼容GPLv3的許可證。
謝謝你的這個話題和所有的答案。 我還遇到了一些計時不准確的問題(Windows 10 + Python 3.9 + 線程)。
解決方案是使用oclock包,並通過wres包(臨時)更改 Windows 系統計時器的分辨率。 該軟件包利用未記錄的 Windows API 函數NtSetTimerResolution (警告:分辨率已在系統范圍內更改)。
僅應用oclock包並不能解決問題。
應用這兩個 python 包后,下面的代碼可以正確且准確地安排定期事件。 如果終止,則恢復原始計時器分辨率。
import threading
import datetime
import time
import oclock
import wres
class Job(threading.Thread):
def __init__(self, interval, *args, **kwargs):
threading.Thread.__init__(self)
# use oclock.Event() instead of threading.Event()
self.stopped = oclock.Event()
self.interval = interval.total_seconds()
self.args = args
self.kwargs = kwargs
def stop(self):
self.stopped.set()
self.join()
def run(self):
prevTime = time.time()
while not self.stopped.wait(self.interval):
now = time.time()
print(now - prevTime)
prevTime = now
# Set system timer resolution to 1 ms
# Automatically restore previous resolution when exit with statement
with wres.set_resolution(10000):
# Create thread with periodic task called every 10 ms
job = Job(interval=datetime.timedelta(seconds=0.010))
job.start()
try:
while True:
time.sleep(1)
# Hit Ctrl+C to terminate main loop and spawned thread
except KeyboardInterrupt:
job.stop()
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.