Подтвердить что ты не робот

Python time.sleep() vs event.wait()

Я хочу выполнить действие с регулярным интервалом в многопоточном приложении Python. Я видел два разных способа сделать это

exit = False
def thread_func(): 
    while not exit:
       action()
       time.sleep(DELAY)

или

exit_flag = threading.Event()
def thread_func(): 
    while not exit_flag.wait(timeout=DELAY):
       action()

Есть ли преимущество в одном направлении над другим? Используете ли вы меньше ресурсов или играете лучше с другими потоками и GIL? Какой из оставшихся потоков в моем приложении более отзывчив?

(Предположим, что некоторые внешние наборы событий exit или exit_flag, и я готов дождаться полной задержки при выключении)

4b9b3361

Ответ 1

Использование exit_flag.wait(timeout=DELAY) будет более гибким, потому что вы мгновенно выйдете из цикла while, когда будет установлен exit_flag. С помощью time.sleep, даже после того, как событие будет установлено, вы будете ждать в вызове time.sleep, пока не посчитаете DELAY секунды.

С точки зрения реализации, Python 2.x и Python 3.x имеют совсем другое поведение. В Python 2.x Event.wait реализован в чистом Python, используя кучу небольших time.sleep вызовов:

from time import time as _time, sleep as _sleep

....
# This is inside the Condition class (Event.wait calls Condition.wait).
def wait(self, timeout=None):
    if not self._is_owned():
        raise RuntimeError("cannot wait on un-acquired lock")
    waiter = _allocate_lock()
    waiter.acquire()
    self.__waiters.append(waiter)
    saved_state = self._release_save()
    try:    # restore state no matter what (e.g., KeyboardInterrupt)
        if timeout is None:
            waiter.acquire()
            if __debug__:
                self._note("%s.wait(): got it", self)
        else:
            # Balancing act:  We can't afford a pure busy loop, so we
            # have to sleep; but if we sleep the whole timeout time,
            # we'll be unresponsive.  The scheme here sleeps very
            # little at first, longer as time goes on, but never longer
            # than 20 times per second (or the timeout time remaining).
            endtime = _time() + timeout
            delay = 0.0005 # 500 us -> initial delay of 1 ms
            while True:
                gotit = waiter.acquire(0)
                if gotit:
                    break
                remaining = endtime - _time()
                if remaining <= 0:
                    break
                delay = min(delay * 2, remaining, .05)
                _sleep(delay)
            if not gotit:
                if __debug__:
                    self._note("%s.wait(%s): timed out", self, timeout)
                try:
                    self.__waiters.remove(waiter)
                except ValueError:
                    pass
            else:
                if __debug__:
                    self._note("%s.wait(%s): got it", self, timeout)
    finally:
        self._acquire_restore(saved_state)

Это на самом деле означает, что использование wait, вероятно, немного больше, чем у процессора, чем просто спящий полный DELAY безоговорочно, но имеет преимущество (потенциально много, в зависимости от того, как долго DELAY) более отзывчивый. Это также означает, что GIL нужно часто повторно приобретать, так что следующий сон может быть запланирован, а time.sleep может освободить GIL для полного DELAY. Теперь, приобретая GIL чаще, окажут заметное влияние на другие потоки вашего приложения? Может быть, а может и нет. Это зависит от того, сколько других потоков работает и какие рабочие нагрузки у них есть. Я предполагаю, что это не будет особенно заметно, если у вас не будет большого количества потоков, или, возможно, другого потока, выполняющего много работы с привязкой к процессору, но его достаточно легко попробовать в обоих направлениях и посмотреть.

В Python 3.x большая часть реализации перемещается в чистый код C:

import _thread # C-module
_allocate_lock = _thread.allocate_lock

class Condition:
    ...
    def wait(self, timeout=None):
        if not self._is_owned():
            raise RuntimeError("cannot wait on un-acquired lock")
        waiter = _allocate_lock()
        waiter.acquire()
        self._waiters.append(waiter)
        saved_state = self._release_save()
        gotit = False
        try:    # restore state no matter what (e.g., KeyboardInterrupt)
            if timeout is None:
                waiter.acquire()
                gotit = True
            else:
                if timeout > 0:
                    gotit = waiter.acquire(True, timeout)  # This calls C code
                else:
                    gotit = waiter.acquire(False)
            return gotit
        finally:
            self._acquire_restore(saved_state)
            if not gotit:
                try:
                    self._waiters.remove(waiter)
                except ValueError:
                    pass

class Event:
    def __init__(self):
        self._cond = Condition(Lock())
        self._flag = False

    def wait(self, timeout=None):
        self._cond.acquire()
        try:
            signaled = self._flag
            if not signaled:
                signaled = self._cond.wait(timeout)
            return signaled
        finally:
            self._cond.release()

И код C, который получает блокировку:

/* Helper to acquire an interruptible lock with a timeout.  If the lock acquire
 * is interrupted, signal handlers are run, and if they raise an exception,
 * PY_LOCK_INTR is returned.  Otherwise, PY_LOCK_ACQUIRED or PY_LOCK_FAILURE
 * are returned, depending on whether the lock can be acquired withing the
 * timeout.
 */
static PyLockStatus
acquire_timed(PyThread_type_lock lock, PY_TIMEOUT_T microseconds)
{
    PyLockStatus r;
    _PyTime_timeval curtime;
    _PyTime_timeval endtime;


    if (microseconds > 0) {
        _PyTime_gettimeofday(&endtime);
        endtime.tv_sec += microseconds / (1000 * 1000);
        endtime.tv_usec += microseconds % (1000 * 1000);
    }


    do {
        /* first a simple non-blocking try without releasing the GIL */
        r = PyThread_acquire_lock_timed(lock, 0, 0);
        if (r == PY_LOCK_FAILURE && microseconds != 0) {
            Py_BEGIN_ALLOW_THREADS  // GIL is released here
            r = PyThread_acquire_lock_timed(lock, microseconds, 1);
            Py_END_ALLOW_THREADS
        }

        if (r == PY_LOCK_INTR) {
            /* Run signal handlers if we were interrupted.  Propagate
             * exceptions from signal handlers, such as KeyboardInterrupt, by
             * passing up PY_LOCK_INTR.  */
            if (Py_MakePendingCalls() < 0) {
                return PY_LOCK_INTR;
            }

            /* If we're using a timeout, recompute the timeout after processing
             * signals, since those can take time.  */
            if (microseconds > 0) {
                _PyTime_gettimeofday(&curtime);
                microseconds = ((endtime.tv_sec - curtime.tv_sec) * 1000000 +
                                (endtime.tv_usec - curtime.tv_usec));

                /* Check for negative values, since those mean block forever.
                 */
                if (microseconds <= 0) {
                    r = PY_LOCK_FAILURE;
                }
            }
        }
    } while (r == PY_LOCK_INTR);  /* Retry if we were interrupted. */

    return r;
}

Эта реализация реагирует и не требует частых пробуждений, которые повторно приобретают GIL, поэтому вы получаете лучшее из обоих миров.

Ответ 2

Python 2. *
Как и @dano, event.wait более отзывчив,
но может быть опасным, когда система время изменяется назад, пока она ждет!
ошибка # 1607041: время ожидания time.wait при изменении часов

Смотрите этот образец:

def someHandler():
   while not exit_flag.wait(timeout=0.100):
       action()

Обычно action() вызывается в 100 мс intrvall.
Но когда вы меняете время ex. один час, то между двумя действиями происходит пауза в один час.

Заключение: когда это позволяет изменить время, вам следует избегать event.wait