簡體   English   中英

制作計時器:threading.Event.wait 的超時不准確 - Python 3.6

[英]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():准確但不可中斷
  • threading.Event.wait():不准確但可中斷

我目前正在考慮一個折衷方案:就像在示例中一樣,創建一個微小的 time.sleep() 循環(間隔為 0.5 毫秒)並使用 if 語句退出循環並在需要時中斷 據我所知,該方法用於 Python 2.x Python time.sleep() vs event.wait()

這是一個冗長的介紹,但我的問題可以總結如下。

  1. 我可以通過外部信號或事件強制線程進程從 time.sleep() 中斷嗎? (這似乎是最有效的。???)

  2. 使 Event.wait() 更准確或減少開銷時間。

  3. 除了 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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM