2015-03-16 30 views
35

Chcę wykonać akcję w regularnych odstępach czasu w mojej wielowątkowej aplikacji Python. Widziałem dwa różne sposoby robienia toPython time.sleep() vs event.wait()

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

lub

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

Czy istnieje jakiś sposób, aby przewaga nad drugim? Czy ktoś zużywa mniej zasobów lub gra ładniej z innymi wątkami i GIL? Który z nich sprawia, że ​​pozostałe wątki w mojej aplikacji są bardziej elastyczne?

(Załóżmy, niektóre zewnętrzne zdarzenia ustala exit lub exit_flag i jestem gotów czekać pełnego opóźnienia podczas zamykania)

+2

Gdzie jest kod, który ustawia 'WYPŁYNIĘCIE flagę? Czy jest on w wywołaniu 'action()' lub w innym wątku, czy może jest wywoływany przez procedurę obsługi sygnału? – tdelaney

+0

Używam 'Event.wait' w tej sytuacji, mimo że python 2.x jest odpytywany w tle. Spanie w, powiedzmy, 1-sekundowych odstępach czasu jest rozsądnie responsywne i mniej inwazyjne. – tdelaney

+0

Pierwsza z nich zmarnuje trochę czasu procesora, na jedną rzecz. – immibis

Odpowiedz

40

Korzystanie exit_flag.wait(timeout=DELAY) będzie bardziej czuły, ponieważ będziesz wyrwać się z pętli while natychmiast po exit_flag jest ustawiony. Po time.sleep, nawet po ustawieniu wydarzenia, będziesz czekać w rozmowie time.sleep, dopóki nie spoczniesz przez DELAY sekund.

Pod względem implementacji, Python 2.x i Python 3.x mają bardzo różne zachowanie. W Pythonie 2.x Event.wait realizowany jest w czystym Pythonie stosując kilka małych time.sleep połączeń:

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) 

To faktycznie oznacza, korzystając wait jest prawdopodobnie nieco więcej CPU-głodny niż tylko bezwarunkowo śpi pełną DELAY, ale ma korzyści są (potencjalnie dużo, w zależności od tego, jak długo jest DELAY) bardziej responsywne. Oznacza to również, że GIL musi być często ponownie pozyskiwany, aby następny sen mógł być zaplanowany, podczas gdy time.sleep może zwolnić GIL dla pełnego DELAY. Czy nabycie GIL częściej będzie miało zauważalny wpływ na inne wątki w twojej aplikacji? Może, a może nie. Zależy to od tego, ile wątków jest uruchomionych i jaki rodzaj obciążenia mają. Domyślam się, że nie będzie to szczególnie zauważalne, chyba że masz dużą liczbę wątków lub inny wątek wykonujący dużo pracy związanej z procesorem, ale jest to łatwe do wypróbowania w obie strony i zobaczenia.

w Pythonie 3.x, wiele realizacji zostanie przeniesiona do czystego kodu 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() 

a kod C, która nabywa Lock:

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

Ta implementacja jest czułe i nie wymaga częstych przebudzeń, które ponownie pozyskują GIL, dzięki czemu można uzyskać najlepsze z obu światów.

+0

czyli czy oznacza to, że 'sleep (DELAY)' jest mniej GIL ciężki? choć nie tak dokładne? – user3012759

+0

@ user3012759 Myślę, że tak, ponieważ każde przebudzenie wewnątrz 'wait' wymagałoby ponownego przejęcia GIL, gdzie' sleep' może po prostu zwolnić go na całe 'DELAY'. – dano

+2

To jest python 2.x (jego znacznie lepiej w 3.x) i jest bardzo zły, szczególnie gdy liczba wątków wzrasta. – tdelaney

3

Python 2. *
Jak @dano powiedział event.wait jest bardziej czuły,
ale może być niebezpieczne gdy czas systemowy zostanie zmieniony tył, podczas gdy czeka!
bug# 1607041: Condition.wait timeout fails on clock change

Zobacz tę próbkę:

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

Normalnie action() będzie wywoływana w 100ms intrvall.
Ale po zmianie czasu np.po godzinie następuje przerwa między dwiema operacjami.

Wniosek: Kiedy jest dozwolone, że czas może być zmiana, należy unikać event.wait