web-dev-qa-db-ja.com

Python 2.7でタイムアウト付きのロックを実装する方法

acquireメソッドが任意のタイムアウトを持つことができるマルチスレッドの目的でPythonにロックを実装する方法はありますか?これまでに見つけた唯一の有効なソリューションはポーリングを使用します。

  • 私はエレガントで非効率的だと思います
  • クリティカルセクションの問題の解決策として、ロックの制限付き待機/進行保証を保持しません

これを実装するためのより良い方法はありますか?

23
Niklas B.

スティーブンのコメント提案について詳しく説明します。

_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
_

注意事項:

  • 2つのthreading.Lock()オブジェクトがあり、1つはthreading.Condition()の内部にあります。
  • condを操作すると、ロックが取得されます。ただし、wait()操作はロックを解除するため、任意の数のスレッドがそれを監視できます。
  • 待機は、時間を追跡するforループ内に埋め込まれています。 _threading.Condition_はタイムアウト以外の理由で通知される可能性があるため、本当に有効期限が切れたい場合は、時間を追跡する必要があります。
  • この状態でも、複数のスレッドがウェイクしてロックを競う可能性があるため、実際のロックを「ポーリング」します。 lock.acquireが失敗した場合、ループは待機に戻ります。
  • この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.')
5
Havok

誰かが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
2

これができるかどうかは疑わしい。

ポーリングなしでこれを実装する場合は、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スレッド。その後、ポーリングタイムアウトは実際にそこにとどまり、時間を繰り返しチェックします。この状況が発生すると予想される場合は、これは悪いことです。長期間。

1
Ben

さて、これはすでに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
0
user3761555

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%)
_
0
frans