2015-03-16 8 views
35

Ich möchte eine Aktion in einem regelmäßigen Intervall in meiner Multi-Thread-Python-Anwendung ausführen. Ich habe zwei verschiedene Arten gesehen, es zu tunPython time.sleep() vs event.wait()

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

oder

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

Gibt es einen Vorteil, einen Weg, über die andere? Nutzt man weniger Ressourcen oder spielt besser mit anderen Threads und der GIL? Welche macht die verbleibenden Threads in meiner App reaktionsfähiger?

(Nehmen Sie ein externes Ereignis setzt exit oder exit_flag, und ich bin bereit, die volle Verzögerung zu warten, während des Herunterfahrens)

+2

vermeiden Wo ist der Code, der die 'exit' Flag setzt? Ist es in der 'action()' -Aufruf oder in einem anderen Thread oder vielleicht von einem Signalhandler aufgerufen? – tdelaney

+0

Ich benutze 'Event.wait' in dieser Situation, obwohl Python 2.x im Hintergrund abfragt. In Intervallen von etwa 1 Sekunde zu schlafen ist einigermaßen reaktionsfähig und weniger aufdringlich. – tdelaney

+0

Der erste wird einige CPU-Zeit verschwenden, für eine Sache. – immibis

Antwort

40

exit_flag.wait(timeout=DELAY) Verwendung wird mehr ansprechbar, weil Sie sofort aus der while-Schleife brechen werden, wenn exit_flag ist eingestellt. Mit time.sleep, selbst nachdem das Ereignis eingestellt ist, werden Sie im time.sleep Anruf warten, bis Sie für DELAY Sekunden geschlafen haben.

Im Hinblick auf die Implementierung haben Python 2.x und Python 3.x ein sehr unterschiedliches Verhalten. In Python 2.x Event.wait in reinem Python mit einer Reihe von kleinen time.sleep Anrufe implementiert:

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) 

Das bedeutet eigentlich wait verwendet, ist wahrscheinlich ein bisschen mehr CPU-hungrig ist als nur die volle DELAY bedingungslos schlafen, hat aber die profitieren (möglicherweise viel, je nachdem wie lange DELAY ist) reaktionsfähiger. Es bedeutet auch, dass die GIL häufig neu erworben werden muss, damit der nächste Schlaf geplant werden kann, während time.sleep die GIL für die volle DELAY freigeben kann. Wird sich der Erwerb der GIL häufiger auf andere Threads in Ihrer Anwendung auswirken? Vielleicht, vielleicht auch nicht. Es hängt davon ab, wie viele andere Threads ausgeführt werden und welche Art von Arbeitslast sie haben. Meine Vermutung ist, dass es nicht besonders auffällt, es sei denn, Sie haben eine hohe Anzahl von Threads oder vielleicht einen anderen Thread, der viel CPU-gebundene Arbeit erledigt, aber es ist einfach genug, um es in beide Richtungen zu probieren und zu sehen.

In Python 3.x wird viel von der Umsetzung der reinen C-Code bewegt:

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

und die C-Code, der die Sperre erwirbt:

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

Diese Implementierung reagiert, und erfordert keine häufigen Wakeups, die das GIL wiedererlangen, so dass Sie das Beste aus beiden Welten erhalten.

+0

also, bedeutet es, dass die 'sleep (DELAY)' weniger GIL schwer ist? wenn auch nicht so genau? – user3012759

+0

@ user3012759 Ich würde es mir denken, da jedes Aufwecken innerhalb von 'wait' das erneute Abrufen der GIL erfordern würde, wobei' sleep' es nur für die Gesamtheit von 'DELAY' freigeben kann. – dano

+2

Dies ist Python 2.x (es ist wesentlich besser in 3.x) und es ist ziemlich schlecht, besonders da die Anzahl der Threads zunimmt. – tdelaney

3

Python 2. *
Wie @dano sagte, ist event.wait mehr ansprechbar,
aber es kann, wenn das System Zeit geändert wird rückwärts gefährlich sein, während es warten wird!
bug# 1607041: Condition.wait timeout fails on clock change

dieses Beispiel Siehe:

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

Normalerweise action() in einem 100ms intrvall genannt wird.
Aber wenn Sie die Zeit ändern ex.eine Stunde dann gibt es eine Pause von einer Stunde zwischen zwei Aktionen.

Fazit: Wenn es erlaubt ist, dass die Zeit ändern kann, sollten Sie event.wait