acquire
メソッドが任意のタイムアウトを持つことができるマルチスレッドの目的でPythonにロックを実装する方法はありますか?これまでに見つけた唯一の有効なソリューションはポーリングを使用します。
これを実装するためのより良い方法はありますか?
スティーブンのコメント提案について詳しく説明します。
_import threading
import time
lock = threading.Lock()
cond = threading.Condition(threading.Lock())
def waitLock(timeout):
with cond:
current_time = start_time = time.time()
while current_time < start_time + timeout:
if lock.acquire(False):
return True
else:
cond.wait(timeout - current_time + start_time)
current_time = time.time()
return False
_
注意事項:
threading.Lock()
オブジェクトがあり、1つはthreading.Condition()
の内部にあります。cond
を操作すると、ロックが取得されます。ただし、wait()
操作はロックを解除するため、任意の数のスレッドがそれを監視できます。threading.Condition
_はタイムアウト以外の理由で通知される可能性があるため、本当に有効期限が切れたい場合は、時間を追跡する必要があります。waitLock
関数の呼び出し元は、lock.release()
の後にcond.notify()
を付けて、待機している他のスレッドにロックの取得を再試行する必要があることを通知する必要があります。これは例には示されていません。スレッドセーフキューを使用する私のバージョン http://docs.python.org/2/library/queue.html およびタイムアウトをサポートするそれらのput/getメソッド。
今までは問題なく動いていましたが、誰かがピアレビューをしてくれたらありがたいです。
"""
Thread-safe lock mechanism with timeout support module.
"""
from threading import ThreadError, current_thread
from Queue import Queue, Full, Empty
class TimeoutLock(object):
"""
Thread-safe lock mechanism with timeout support.
"""
def __init__(self, mutex=True):
"""
Constructor.
Mutex parameter specifies if the lock should behave like a Mutex, and
thus use the concept of thread ownership.
"""
self._queue = Queue(maxsize=1)
self._owner = None
self._mutex = mutex
def acquire(self, timeout=0):
"""
Acquire the lock.
Returns True if the lock was succesfully acquired, False otherwise.
Timeout:
- < 0 : Wait forever.
- 0 : No wait.
- > 0 : Wait x seconds.
"""
th = current_thread()
try:
self._queue.put(
th, block=(timeout != 0),
timeout=(None if timeout < 0 else timeout)
)
except Full:
return False
self._owner = th
return True
def release(self):
"""
Release the lock.
If the lock is configured as a Mutex, only the owner thread can release
the lock. If another thread attempts to release the lock a
ThreadException is raised.
"""
th = current_thread()
if self._mutex and th != self._owner:
raise ThreadError('This lock isn\'t owned by this thread.')
self._owner = None
try:
self._queue.get(False)
return True
except Empty:
raise ThreadError('This lock was released already.')
誰かがPython> = 3.2 APIを必要とする場合:
import threading
import time
class Lock(object):
_lock_class = threading.Lock
def __init__(self):
self._lock = self._lock_class()
self._cond = threading.Condition(threading.Lock())
def acquire(self, blocking=True, timeout=-1):
if not blocking or timeout == 0:
return self._lock.acquire(False)
cond = self._cond
lock = self._lock
if timeout < 0:
with cond:
while True:
if lock.acquire(False):
return True
else:
cond.wait()
else:
with cond:
current_time = time.time()
stop_time = current_time + timeout
while current_time < stop_time:
if lock.acquire(False):
return True
else:
cond.wait(stop_time - current_time)
current_time = time.time()
return False
def release(self):
with self._cond:
self._lock.release()
self._cond.notify()
__enter__ = acquire
def __exit__(self, t, v, tb):
self.release()
class RLock(Lock):
_lock_class = threading.RLock
これができるかどうかは疑わしい。
ポーリングなしでこれを実装する場合は、OSがスレッドがブロックされていることを認識している必要があり、しばらくしてスレッドのブロックを解除するには、OSがタイムアウトを認識している必要があります。そのためには、OSにサポートがすでに存在している必要があります。これをPythonレベルで実装することはできません。
(OSレベルまたはアプリレベルのいずれかでスレッドをブロックし、適切なタイミングで別のスレッドによって起動できるメカニズムを使用できますが、効果的にポーリングするには、他のスレッドが必要です)
一般に、スレッドは、ブロックが解除されたことを認識するためにコンテキストスイッチが発生するまで無制限の時間を待機する必要があるため、ロックの真に制限された待機/進行の保証はありません。したがって、進行中のCPU競合の量に上限を設けることができない限り、タイムアウトを使用して厳しいリアルタイムの期限に達することはできません。しかし、おそらくそれは必要ありません。そうでなければ、Pythonで実装されたロックを使用することを夢見ないでしょう。
Python GIL(Global Interpreter Lock)により、これらのポーリングベースのソリューションは、おそらく(実装方法に応じて)思ったほど非効率的ではなく、制限もありません( CPythonまたはPyPyのいずれかを使用しています)。
一度に実行されるスレッドは1つだけであり、定義上、実行する別のスレッド(待機しているロックを保持するスレッド)があります。 GILは、大量のバイトコードを実行するために1つのスレッドによってしばらく保持され、その後ドロップされて再取得され、他の誰かにそれを実行する機会が与えられます。したがって、blocked-with-timeoutスレッドが、時間をチェックして他のスレッドに譲るループ内にある場合、GILを取得したときに頻繁にウェイクアップし、ほとんどすぐに他の誰かにドロップしてブロックします。再びGIL。このスレッドは、とにかくGILでターンを取得したときにのみウェイクアップできるため、タイムアウトが魔法のように完璧であったとしても実行を再開できるため、タイムアウトの期限が切れるとすぐにこのチェックも実行します。
これが多くの非効率を引き起こすのは、スレッドがロック保持スレッドを待機してブロックされている場合のみです。このスレッドは、別のPythonスレッド(Python $ ===スレッド(Python $ ===)によって引き起こされないものを待機してブロックされています。たとえば、IOでブロックされます)、他に実行可能なスレッドはありませんPythonスレッド。その後、ポーリングタイムアウトは実際にそこにとどまり、時間を繰り返しチェックします。この状況が発生すると予想される場合は、これは悪いことです。長期間。
さて、これはすでにpython 3.2以上で実装されています: https://docs.python.org/3/library/threading。 html スレッドを探します。TIMEOUT_MAX
しかし、私はfransのバージョンよりもテストケースを改善しました... py3.2以上を使用している場合、これはすでに時間の無駄ですが:
from unittest.mock import patch, Mock
import unittest
import os
import sys
import logging
import traceback
import threading
import time
from Util import ThreadingUtil
class ThreadingUtilTests(unittest.TestCase):
def setUp(self):
pass
def tearDown(self):
pass
# https://www.pythoncentral.io/pythons-time-sleep-pause-wait-sleep-stop-your-code/
def testTimeoutLock(self):
faulted = [False, False, False]
def locking_thread_fn(threadId, lock, duration, timeout):
try:
threadName = "Thread#" + str(threadId)
with ThreadingUtil.TimeoutLock(threadName, lock, timeout=timeout, raise_on_timeout=True):
print('%x: "%s" begins to work..' % (threading.get_ident(), threadName))
time.sleep(duration)
print('%x: "%s" finished' % (threading.get_ident(), threadName))
except:
faulted[threadId] = True
_lock = ThreadingUtil.TimeoutLock.lock()
_sleepDuration = [5, 10, 1]
_threads = []
for i in range(3):
_duration = _sleepDuration[i]
_timeout = 6
print("Wait duration (sec): " + str(_duration) + ", Timeout (sec): " + str(_timeout))
_worker = threading.Thread(
target=locking_thread_fn,
args=(i, _lock, _duration, _timeout)
)
_threads.append(_worker)
for t in _threads: t.start()
for t in _threads: t.join()
self.assertEqual(faulted[0], False)
self.assertEqual(faulted[1], False)
self.assertEqual(faulted[2], True)
「Util」フォルダの下に「ThreadingUtil.py」があります。
import time
import threading
# https://stackoverflow.com/questions/8392640/how-to-implement-a-lock-with-a-timeout-in-python-2-7
# https://docs.python.org/3.4/library/asyncio-sync.html#asyncio.Condition
# https://stackoverflow.com/questions/28664720/how-to-create-global-lock-semaphore-with-multiprocessing-pool-in-python
# https://hackernoon.com/synchronization-primitives-in-python-564f89fee732
class TimeoutLock(object):
''' taken from https://stackoverflow.com/a/8393033/1668622
'''
class lock:
def __init__(self):
self.owner = None
self.lock = threading.Lock()
self.cond = threading.Condition()
def _release(self):
self.owner = None
self.lock.release()
with self.cond:
self.cond.notify()
def __init__(self, owner, lock, timeout=1, raise_on_timeout=False):
self._owner = owner
self._lock = lock
self._timeout = timeout
self._raise_on_timeout = raise_on_timeout
# http://effbot.org/zone/python-with-statement.htm
def __enter__(self):
self.acquire()
return self
def __exit__(self, type, value, tb):
''' will only be called if __enter__ did not raise '''
self.release()
def acquire(self):
if self._raise_on_timeout:
if not self._waitLock():
raise RuntimeError('"%s" could not aquire lock within %d sec'
% (self._owner, self._timeout))
else:
while True:
if self._waitLock():
break
print('"%s" is waiting for "%s" and is getting bored...'
% (self._owner, self._lock.owner))
self._lock.owner = self._owner
def release(self):
self._lock._release()
def _waitLock(self):
with self._lock.cond:
_current_t = _start_t = time.time()
while _current_t < _start_t + self._timeout:
if self._lock.lock.acquire(False):
return True
else:
self._lock.cond.wait(self._timeout - _current_t + _start_t)
_current_t = time.time()
return False
SingleNegationElimination の答えを受け取り、次のようにwith
ステートメントで使用できるクラスを作成しました。
_global_lock = timeout_lock()
...
with timeout_lock(owner='task_name', lock=global_lock):
do()
some.stuff()
_
このようにして、タイムアウトが期限切れになった場合(デフォルト= 1秒)にのみ警告し、調査のためにロックの所有者を表示します。
このように使用すると、タイムアウト後に例外がスローされます。
_with timeout_lock(owner='task_name', lock=global_lock, raise_on_timeout=True):
do()
some.stuff()
_
timeout_lock.lock()
インスタンスは一度作成する必要があり、スレッド間で使用できます。
これがクラスです-それは私にとってはうまくいきますが、コメントして改善してください:
_class timeout_lock:
''' taken from https://stackoverflow.com/a/8393033/1668622
'''
class lock:
def __init__(self):
self.owner = None
self.lock = threading.Lock()
self.cond = threading.Condition()
def _release(self):
self.owner = None
self.lock.release()
with self.cond:
self.cond.notify()
def __init__(self, owner, lock, timeout=1, raise_on_timeout=False):
self._owner = owner
self._lock = lock
self._timeout = timeout
self._raise_on_timeout = raise_on_timeout
def __enter__(self):
self.acquire()
return self
def __exit__(self, type, value, tb):
''' will only be called if __enter__ did not raise '''
self.release()
def acquire(self):
if self._raise_on_timeout:
if not self._waitLock():
raise RuntimeError('"%s" could not aquire lock within %d sec'
% (self._owner, self._timeout))
else:
while True:
if self._waitLock():
break
print('"%s" is waiting for "%s" and is getting bored...'
% (self._owner, self._lock.owner))
self._lock.owner = self._owner
def release(self):
self._lock._release()
def _waitLock(self):
with self._lock.cond:
_current_t = _start_t = time.time()
while _current_t < _start_t + self._timeout:
if self._lock.lock.acquire(False):
return True
else:
self._lock.cond.wait(self._timeout - _current_t + _start_t)
_current_t = time.time()
return False
_
スレッドが実際に干渉せず、できるだけ早く通知を受け取るのを待たないようにするために、すべてのスレッドの実行に必要な時間を合計する小さなマルチスレッドテストを作成しました。
_def test_lock_guard():
import random
def locking_thread_fn(name, lock, duration, timeout):
with timeout_lock(name, lock, timeout=timeout):
print('%x: "%s" begins to work..' % (threading.get_ident(), name))
time.sleep(duration)
print('%x: "%s" finished' % (threading.get_ident(), name))
_lock = timeout_lock.lock()
_threads = []
_total_d = 0
for i in range(3):
_d = random.random() * 3
_to = random.random() * 2
_threads.append(threading.Thread(
target=locking_thread_fn, args=('thread%d' % i, _lock, _d, _to)))
_total_d += _d
_t = time.time()
for t in _threads: t.start()
for t in _threads: t.join()
_t = time.time() - _t
print('duration: %.2f sec / expected: %.2f (%.1f%%)'
% (_t, _total_d, 100 / _total_d * _t))
_
出力は次のとおりです。
_7f940fc2d700: "thread0" begins to work..
"thread2" is waiting for "thread0" and is getting bored...
"thread2" is waiting for "thread0" and is getting bored...
"thread2" is waiting for "thread0" and is getting bored...
7f940fc2d700: "thread0" finished
7f940f42c700: "thread1" begins to work..
"thread2" is waiting for "thread1" and is getting bored...
"thread2" is waiting for "thread1" and is getting bored...
7f940f42c700: "thread1" finished
"thread2" is waiting for "None" and is getting bored...
7f940ec2b700: "thread2" begins to work..
7f940ec2b700: "thread2" finished
duration: 5.20 sec / expected: 5.20 (100.1%)
_