Реализация блокировки Python (с модулем потоков) - PullRequest
2 голосов
/ 05 мая 2010

Это, вероятно, элементарный вопрос, но я новичок в многопоточном программировании на Python и не совсем уверен, какова правильная практика.

Должен ли я создавать один объект блокировки (глобально или передаваемый) и использовать его везде, где мне нужно сделать блокировку? Или я должен создать несколько экземпляров блокировки в каждом из классов, где я буду их использовать. Возьмите эти 2 элементарных примера кода, в каком направлении лучше идти? Основное отличие состоит в том, что один экземпляр блокировки используется как во втором классе, так и в классе B, а в первом - несколько экземпляров.

Образец 1

class A():
    def __init__(self, theList):
        self.theList = theList
        self.lock = threading.Lock()

    def poll(self):
        while True:
            # do some stuff that eventually needs to work with theList
            self.lock.acquire()
            try:
                self.theList.append(something)
            finally:
                self.lock.release()





class B(threading.Thread):
    def __init__(self,theList):
        self.theList = theList
        self.lock = threading.Lock()
        self.start()


    def run(self):
        while True:
            # do some stuff that eventually needs to work with theList
            self.lock.acquire()
            try:
                self.theList.remove(something)
            finally:
                self.lock.release()



if __name__ == "__main__":
    aList = []
    for x in range(10):
        B(aList)

    A(aList).poll()

Образец 2

class A():
    def __init__(self, theList,lock):
        self.theList = theList
        self.lock = lock

    def poll(self):
        while True:
            # do some stuff that eventually needs to work with theList
            self.lock.acquire()
            try:
                self.theList.append(something)
            finally:
                self.lock.release()



class B(threading.Thread):
    def __init__(self,theList,lock):
        self.theList = theList
        self.lock = lock
        self.start()


    def run(self):
        while True:
            # do some stuff that eventually needs to work with theList
            self.lock.acquire()
            try:
                self.theList.remove(something)
            finally:
                self.lock.release()



if __name__ == "__main__":
    lock = threading.Lock()
    aList = []
    for x in range(10):
        B(aList,lock)

    A(aList,lock).poll()

Ответы [ 2 ]

9 голосов
/ 05 мая 2010

Если вы используете отдельный объект блокировки в каждом классе, то вы рискуете получить взаимоблокировку, например, если одна операция запрашивает блокировку для A, а затем запрашивает блокировку для B, в то время как другая операция запрашивает B и затем A.

Если вы используете одиночную блокировку, то вы принудительно вводите код в один поток, когда разные операции могут выполняться параллельно. Это не всегда так серьезно в Python (который в любом случае имеет глобальную блокировку), как в других языках, но, скажем, вы должны были удерживать глобальную блокировку во время записи в файл, Python выпустил бы GIL, но вы бы заблокировали все еще.

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

Что касается ваших конкретных примеров, то первый пример просто разбит. Если вы блокируете операции на theList, вы должны использовать одну и ту же блокировку каждый раз, иначе вы ничего не блокируете. Это может не иметь значения, так как list.append и list.remove по сути являются атомарными, но если вам нужно заблокировать доступ к списку, вы должны быть уверены, что каждый раз будете использовать одну и ту же блокировку. Лучший способ сделать это - сохранить список и блокировку в качестве атрибутов класса и заставить весь доступ к списку проходить через методы содержащего класса. Затем передайте класс контейнера вокруг списка или блокировки.

9 голосов
/ 05 мая 2010

В общем случае единичная глобальная блокировка менее эффективна (больше споров), но безопаснее (нет риска тупиковой ситуации), если она RLock (реентерабельная), а не обычная Lock.

Потенциальные проблемы возникают, когда поток, выполняющийся во время удержания блокировки, пытается получить другую (или такую ​​же) блокировку, например, вызывая другой метод, содержащий вызов acquire. Если поток, который уже удерживает блокировку, пытается получить ее снова, он будет блокироваться навсегда, если блокировка простая Lock, но будет проходить гладко, если она немного сложнее RLock - поэтому последняя называется reentrant , поскольку удерживающая его нить может снова «войти» (получить блокировку). По сути, RLock отслеживает , какой поток удерживает его, и сколько раз поток получает блокировку, в то время как более простая блокировка не хранит такую ​​информацию.

При множественных блокировках проблема взаимоблокировки возникает, когда один поток пытается получить блокировку A, а затем блокировку B, в то время как другой пытается получить сначала блокировку B, а затем блокировку A. Если это произойдет, то рано или поздно вы окажетесь в ситуация, когда первая блокировка удерживает A, вторая - B, и каждый пытается получить блокировку, которую удерживает другой - так что оба блокируются навсегда.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...