制作计时器:threading.Event.wait的超时不准确性 - Python 3.6

时间:2018-02-26 08:48:31

标签: python multithreading timer sleep periodic-task

首先,我是Python的新手,并不熟悉它的功能。我一直主要使用MATLAB。

PC简要规范:Windows 10,Intel i7

我正在尝试创建一个定时器类来定期执行MATLAB所具有的功能,这显然是从Java定时器中借用的。 MATLAB定时器的分辨率约为1 ms,在任何情况下我都没有看到它超过2 ms。事实上,它对我的​​项目来说足够准确。

最近,我计划转向Python,因为MATLAB的并行计算和Web访问功能很差。然而,遗憾的是,与MATLAB相比,Python的标准软件包提供了一些低级别的计时器(threading.Timer),我必须自己制作计时器类。首先,我提到了QnA Executing periodic actions in Python [duplicate] Michael Anderson 建议的解决方案给出了漂移校正的简单概念。他用time.sleep()来保持这段时间。该方法非常准确,有时显示出比MATLAB计时器更高的精度。约。 0.5毫秒分辨率。但是,在time.sleep()中捕获期间,定时器不能被中断(暂停或恢复)。但我有时不得不立即停止,无论是否处于睡眠状态()。

我发现问题的解决方案是在线程包中使用Event类。请参阅Python threading.timer - repeat function every 'n' seconds 。使用Event.wait()的超时功能,我可以在执行之间留出时间间隔,它用于保持周期。也就是说,事件通常被清除,以便wait(timeout)可以像time.sleep(interval)一样,我可以在需要时通过设置事件立即退出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语句和 break 退出循环需要的时候。据我所知,该方法用于Python 2.x Python time.sleep() vs event.wait()

这是一个冗长的介绍,但我的问题可归纳如下。

  1. 我可以通过外部信号或事件强制线程进程从time.sleep()中断吗? (这似乎是最有效的。???)

  2. 使Event.wait()更准确或减少开销时间。

  3. 除了sleep()和Event.wait()方法之外,还有更好的方法来提高计时精度。

  4. 非常感谢。

3 个答案:

答案 0 :(得分:0)

我遇到了Event.wait()的同一计时问题。我想到的解决方案是创建一个模拟threading.Event的类。在内部,它使用time.sleep()循环和忙碌循环的组合来大大提高精度。睡眠循环在单独的线程中运行,因此仍可以立即中断主线程中的阻塞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

答案 1 :(得分:0)

Chris D 的自定义 Event 类效果非常好!出于实用目的,我将它包含在一个可安装的包中(https://github.com/ovinc/oclock,使用 pip install oclock 安装),其中还包含其他计时工具。从 oclock 的 1.3.0 版开始,可以使用 Chris D 的回答中讨论的自定义 Event 类,例如

from oclock import Event
event = Event()
event.wait(1)

使用 set() 类的常用 clear()is_set()wait()Event 方法。

计时精度比 threading.Event 好得多,尤其是在 Windows 中。例如,在具有 1000 个重复循环的 Windows 机器上,threading.Event 的循环持续时间为 7 毫秒,oclock.Event 的标准偏差小于 0.01 毫秒。克里斯 D 的道具!

注意oclock 软件包在 GPLv3 许可下与 StackOverflow 的 CC BY-SA 4.0 兼容。

答案 2 :(得分:0)

感谢您的主题和所有答案。我还遇到了一些计时不准确的问题(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()