マルチスレッドPythonアプリケーションで定期的にアクションを実行したい。私はそれを行う2つの異なる方法を見てきました
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
を設定し、シャットダウン中に完全な遅延を待つと仮定します)
exit_flag
が設定されている場合、すぐにwhileループから抜け出すため、exit_flag.wait(timeout=DELAY)
を使用すると応答が速くなります。 time.sleep
を使用すると、イベントが設定された後でも、DELAY
秒間スリープするまでtime.sleep
呼び出しで待機します。
実装に関しては、Python 2.xとPython 3.xの動作は大きく異なります。 Python 2.x Event.wait
では、小さなtime.sleep
呼び出しの束を使用して、純粋なPythonに実装されています。
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
を無条件でスリープするよりもCPUを多く消費することを意味しますが、利点があります(DELAY
の長さに応じて、多くの場合)より敏感。また、time.sleep
が完全なDELAY
のGILをリリースできるようにしながら、次のスリープをスケジュールできるように、GILを頻繁に再取得する必要があることも意味します。さて、GILをより頻繁に取得すると、アプリケーションの他のスレッドに顕著な影響がありますか?多分そうでないかもしれません。それは、実行中の他のスレッドの数と、それらがどのような作業負荷を持っているかに依存します。私の推測では、スレッドの数が多い場合や、おそらくCPUにバインドされた多くの作業を行う別のスレッドがない限り、特に目立ちませんが、両方の方法で試してみるのは簡単です。
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を再取得する頻繁なウェイクアップを必要としないため、両方の利点を最大限に活用できます。
Python 2. *
@ danoが言ったように、event.waitの方が反応がよく、
しかし、それは危険な可能性があるシステムが時間を後方に変更するのとき、待機中です!
バグ番号1607041:クロック変更時にCondition.waitタイムアウトが失敗する
このサンプルをご覧ください:
def someHandler():
while not exit_flag.wait(timeout=0.100):
action()
通常、action()
は100msのintrvallで呼び出されます。
ただし、時間を変更するとex。 1時間後、2つのアクションの間に1時間の休止があります。
結論:時間を変更できる場合は、event.wait
を避ける必要があります
Event.wait()メソッドを単独で呼び出すことができることに注意してください。
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")
では、なぜマルチスレッドの外でsleep()の代わりにwait()を使用しないのですか?一言で言えば、禅。 (もちろん。)コードの明瞭さは重要です。