Fcntl.flock - как реализовать тайм-аут?

Я использую python 2.7

Я хочу создать оберточную функцию вокруг fcntl.flock(), которая будет истекать через заданный интервал:

wrapper_function(timeout):

Я попытался вызвать другой поток и использовать thread.join(timeout), но кажется, что fcntl.flock() продолжает блокировать:

def GetLock(self, timeout):
    """Returns true if lock is aquired, false if lock is already in use"""
    self.__lock_file = open('proc_lock', 'w')

    def GetLockOrTimeOut():
        print 'ProcessLock: Acquiring Lock'            
        fcntl.flock(self.__lock_file.fileno(), fcntl.LOCK_EX)
        print 'ProcessLock: Lock Acquired'

    thread = threading.Thread(target=GetLockOrTimeOut)
    thread.start()
    thread.join(timeout)

    if thread.isAlive():
        print 'GetLock timed out'
        return False
    else:
        return True

Я рассмотрел решения для прекращения потоков, наиболее популярным решением является подклассификация threading.thread и добавление функции для создания исключения в потоке. Тем не менее, я наткнулся на ссылку в которой говорится, что этот метод не будет работать с собственными вызовами, и я уверен, что fcntl.flock() вызывает собственную функцию. Предложения?

Контекст: я использую блокировку файлов для создания одного экземпляра приложения, но я не хочу, чтобы второй экземпляр приложения сидел и зависал, пока первый экземпляр не завершится.

Ответ 1

Тайм-ауты для системных вызовов выполняются с помощью сигналов. Большинство системных вызовов блокировки возвращаются с EINTR, когда происходит сигнал, поэтому вы можете использовать alarm для выполнения тайм-аутов.

Вот менеджер контекста, который работает с большинством системных вызовов, вызывая повышение IOError из системного вызова блокировки, если он занимает слишком много времени.

import signal, errno
from contextlib import contextmanager
import fcntl

@contextmanager
def timeout(seconds):
    def timeout_handler(signum, frame):
        pass

    original_handler = signal.signal(signal.SIGALRM, timeout_handler)

    try:
        signal.alarm(seconds)
        yield
    finally:
        signal.alarm(0)
        signal.signal(signal.SIGALRM, original_handler)

with timeout(1):
    f = open("test.lck", "w")
    try:
        fcntl.flock(f.fileno(), fcntl.LOCK_EX)
    except IOError, e:
        if e.errno != errno.EINTR:
            raise e
        print "Lock timed out"

Ответ 2

Я уверен, что есть несколько способов, но как насчет использования блокировки без блокировки? После некоторых n попыток сдаться и выйти?

Чтобы использовать блокировку без блокировки, включите флаг fcntl.LOCK_NB, как в:

fcntl.flock(self.__lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)

Ответ 3

Я поклонник обхода, чтобы свалить сюда, поскольку попытка блокировки блокировки с таймаутом требует изменений в глобальном состоянии, что затрудняет рассуждение о вашей программе, особенно если задействована потоковая передача.

Вы можете отключить подпроцесс и реализовать аварийный сигнал, как указано выше, или просто выполнить http://man7.org/linux/man-pages/man1/flock.1.html

import subprocess
def flock_with_timeout(fd, timeout, shared=True):
    rc = subprocess.call(['flock', '--shared' if shared else '--exclusive', '--timeout', str(timeout), str(fd)])
    if rc != 0:
        raise Exception('Failed to take lock')

Если у вас есть достаточно новая версия flock, вы можете использовать -E, чтобы указать другой код выхода для команды в противном случае, но не удалось сделать блокировку после таймаута, так что вы можете узнать, не сработала ли команда для некоторых другая причина.

Ответ 4

Для Python 3. 5+ решение Гленна Мейнарда больше не работает из-за PEP-475. Это модифицированная версия:

import signal, errno
from contextlib import contextmanager
import fcntl

@contextmanager
def timeout(seconds):
    def timeout_handler(signum, frame):
        # Now that flock retries automatically when interrupted, we need
        # an exception to stop it
        # This exception will propagate on the main thread, make sure you're calling flock there
        raise InterruptedError

    original_handler = signal.signal(signal.SIGALRM, timeout_handler)

    try:
        signal.alarm(seconds)
        yield
    finally:
        signal.alarm(0)
        signal.signal(signal.SIGALRM, original_handler)

with timeout(1):
    f = open("test.lck", "w")
    try:
        fcntl.flock(f.fileno(), fcntl.LOCK_EX)
    except InterruptedError:
        # Catch the exception raised by the handler
        # If we weren't raising an exception, flock would automatically retry on signals
        print("Lock timed out")