web-dev-qa-db-fra.com

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

Je souhaite effectuer une action à intervalle régulier dans mon application multi-thread Python. J'ai vu deux méthodes différentes pour le faire.

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

ou

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

Y a-t-il un avantage à aller d'une manière à l'autre? Est-ce qu'on utilise moins de ressources ou est-il plus agréable avec d'autres threads et le GIL? Lequel rend les threads restants dans mon application plus réactifs?

(Supposons que certains ensembles d’événements externes exit ou exit_flag, et je suis prêt à attendre tout le délai de fermeture)

60
AShelly

Utiliser exit_flag.wait(timeout=DELAY) sera plus réactif, car vous sortirez instantanément de la boucle while lorsque exit_flag est réglé. Avec time.sleep, même après le paramétrage de l'événement, vous allez attendre dans le time.sleep appelez jusqu'à ce que vous ayez dormi pendant DELAY secondes.

En termes d’implémentation, Python 2.x et Python 3.x ont un comportement très différent. Dans Python 2. X Event.wait est implémenté en pure Python en utilisant un tas de petits time.sleep appels:

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)

Cela signifie en fait que l'utilisation de wait est probablement un peu plus gourmand en ressources processeur que de simplement dormir sans condition DELAY, mais présente l'avantage d'être potentiellement (potentiellement beaucoup, en fonction de combien de temps DELAY est) plus sensible. Cela signifie également que le GIL doit être fréquemment ré-acquis, afin que le prochain sommeil puisse être planifié, alors que time.sleep peut libérer le GIL pour la totalité DELAY. Maintenant, l’acquisition de la licence GIL aura-t-elle plus souvent un effet notable sur les autres threads de votre application? Peut etre ou peut etre pas. Cela dépend du nombre d'autres threads en cours d'exécution et du type de charge de travail qu'ils ont. Je suppose que cela ne sera pas particulièrement visible sauf si vous avez un nombre élevé de threads, ou peut-être un autre thread effectuant beaucoup de travail lié au processeur, mais il est assez facile de l'essayer dans les deux sens.

Dans Python 3.x, une grande partie de la mise en œuvre est déplacée vers le code C pur:

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()

Et le code C qui acquiert le verrou:

/* 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;
}

Cette implémentation est réactive et ne nécessite pas de réveils fréquents qui ré-acquièrent le GIL, vous obtenez ainsi le meilleur des deux mondes.

62
dano

Python 2. *
Comme @dano a dit, event.wait est plus réactif,
mais cela peut être dangereux lorsque le temps système est modifié en arrière , en attendant!
bug n ° 1607041: le délai d'attente Condition.wait échoue lors du changement d'horloge

Voir cet échantillon:

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

Normalement, action() sera appelé dans un intervalle de 100 ms.
Mais quand vous changez le temps ex. une heure, puis il y a une pause d'une heure entre deux actions.

Conclusion: Quand il est permis que le temps puisse être changé, vous devriez éviter event.wait

6
jeb

Il est intéressant de noter que la méthode event.wait () peut être invoquée seule:

from threading import Event # Needed for the  wait() method
from time import sleep     

print("\n Live long and prosper!")
sleep(1)               # Conventional sleep() Method.
print("\n Just let that soak in..")   
Event().wait(3.0) # wait() Method, useable sans thread.
print("\n Make it So! = )\n")

Alors pourquoi -pas utiliser wait () comme alternative à sleep () en dehors du multi-threading? En un mot, Zen. (Bien sûr.) La clarté du code est une chose importante.

2
Science_1