如何使用 Python 中的锁来保护对象?
How to protect an object using a lock in Python?
我遇到了需要以下模式的功能:
from threading import Lock
the_list = []
the_list_lock = Lock()
并使用它:
with the_list_lock:
the_list.append("New Element")
不幸的是,这不需要我获取锁,我可以直接访问对象。我想要一些保护来防止这种情况(我只是人类。)有没有标准的方法来做到这一点?我自己的方法是创建一个 HidingLock
class 可以这样使用:
the_list = HidingLock([])
with the_list as l:
l.append("New Element")
但它感觉非常基础,要么它应该存在于标准库中,要么它是一种非常非常规的锁使用方式。
我目前的解决方案(我在问题中谈到的那个)是这样的:
import threading
class HidingLock(object):
def __init__(self, obj, lock=None):
self.lock = lock or threading.RLock()
self._obj = obj
def __enter__(self):
self.lock.acquire()
return self._obj
def __exit__(self, exc_type, exc_value, traceback):
self.lock.release()
def set(self, obj):
with self:
self._obj = obj
下面是如何使用它的:
locked_list = HidingLock(["A"])
with locked_list as l:
l.append("B")
我认为标准库中没有任何内容的原因是因为它需要做出铸铁访问保证。提供任何不足都会给人一种错误的安全感,这可能会导致同样多的并发问题。
如果不做出实质性的性能牺牲,几乎不可能做出这些保证。因此,留给用户考虑他们将如何管理并发问题。这符合 Python 的哲学之一 "we're all consenting adults"。也就是说,如果您正在编写 class 我认为您应该知道在访问属性之前需要获取锁的属性是合理的。或者,如果您真的那么担心,请编写一个 wrapper/proxy class 来控制对基础对象的所有访问。
对于您的示例,目标对象可能会通过多种方式意外逃脱。如果程序员没有对他们 writing/maintaining 的代码给予足够的关注,那么这个 HiddenLock
可能会提供错误的安全感。例如:
with the_lock as obj:
pass
obj.func() # erroneous
with the_lock as obj:
return obj.func() # possibly erroneous
# What if the return value of `func' contains a self reference?
with the_lock as obj:
obj_copy = obj[:]
obj_copy[0] = 2 # erroneous?
最后一个特别有害。此代码是否线程安全不取决于 with 块内的代码,甚至不取决于块之后的代码。相反,obj
的 class 的实现将意味着此代码是否是线程安全的。例如,如果 obj
是 list
那么这是安全的,因为 obj[:]
创建了一个副本。但是,如果 obj
是 numpy.ndarray
,则 obj[:]
会创建一个视图,因此该操作是不安全的。
实际上,如果 obj
的内容是可变的,那么这可能是不安全的(例如 obj_copy[0].mutate()
)。
创建具有 list
并使用 threading.Lock
实现所需 class 方法的 shared_list
怎么样:
import threading
class SharedList(object):
def __init__(self, iterable=None):
if iterable is not None:
self.list = list(iterable)
else:
self.list = list()
self.lock = threading.Lock()
self.index = None
def append(self, x):
with self.lock:
self.list.append(x)
def __iter__(self):
shared_iterator = SharedList()
shared_iterator.list = self.list
shared_iterator.lock = self.lock
shared_iterator.index = 0
return shared_iterator
def next(self):
with self.lock:
if self.index < len(self.list):
result = self.list[self.index]
self.index += 1
else:
raise StopIteration
return result
# Override other methods
if __name__ == '__main__':
shared_list = SharedList()
for x in range(1, 4):
shared_list.append(x)
for entry in shared_list:
print entry
输出
1
2
3
正如 Georg Shölly 在评论中指出的那样,这将需要大量工作来实现每个方法。但是,如果您只需要一个可以追加然后迭代的列表,则此示例提供了起点。
那你就可以写
the_list = SharedList()
the_list.append("New Element")
我遇到了需要以下模式的功能:
from threading import Lock
the_list = []
the_list_lock = Lock()
并使用它:
with the_list_lock:
the_list.append("New Element")
不幸的是,这不需要我获取锁,我可以直接访问对象。我想要一些保护来防止这种情况(我只是人类。)有没有标准的方法来做到这一点?我自己的方法是创建一个 HidingLock
class 可以这样使用:
the_list = HidingLock([])
with the_list as l:
l.append("New Element")
但它感觉非常基础,要么它应该存在于标准库中,要么它是一种非常非常规的锁使用方式。
我目前的解决方案(我在问题中谈到的那个)是这样的:
import threading
class HidingLock(object):
def __init__(self, obj, lock=None):
self.lock = lock or threading.RLock()
self._obj = obj
def __enter__(self):
self.lock.acquire()
return self._obj
def __exit__(self, exc_type, exc_value, traceback):
self.lock.release()
def set(self, obj):
with self:
self._obj = obj
下面是如何使用它的:
locked_list = HidingLock(["A"])
with locked_list as l:
l.append("B")
我认为标准库中没有任何内容的原因是因为它需要做出铸铁访问保证。提供任何不足都会给人一种错误的安全感,这可能会导致同样多的并发问题。
如果不做出实质性的性能牺牲,几乎不可能做出这些保证。因此,留给用户考虑他们将如何管理并发问题。这符合 Python 的哲学之一 "we're all consenting adults"。也就是说,如果您正在编写 class 我认为您应该知道在访问属性之前需要获取锁的属性是合理的。或者,如果您真的那么担心,请编写一个 wrapper/proxy class 来控制对基础对象的所有访问。
对于您的示例,目标对象可能会通过多种方式意外逃脱。如果程序员没有对他们 writing/maintaining 的代码给予足够的关注,那么这个 HiddenLock
可能会提供错误的安全感。例如:
with the_lock as obj:
pass
obj.func() # erroneous
with the_lock as obj:
return obj.func() # possibly erroneous
# What if the return value of `func' contains a self reference?
with the_lock as obj:
obj_copy = obj[:]
obj_copy[0] = 2 # erroneous?
最后一个特别有害。此代码是否线程安全不取决于 with 块内的代码,甚至不取决于块之后的代码。相反,obj
的 class 的实现将意味着此代码是否是线程安全的。例如,如果 obj
是 list
那么这是安全的,因为 obj[:]
创建了一个副本。但是,如果 obj
是 numpy.ndarray
,则 obj[:]
会创建一个视图,因此该操作是不安全的。
实际上,如果 obj
的内容是可变的,那么这可能是不安全的(例如 obj_copy[0].mutate()
)。
创建具有 list
并使用 threading.Lock
实现所需 class 方法的 shared_list
怎么样:
import threading
class SharedList(object):
def __init__(self, iterable=None):
if iterable is not None:
self.list = list(iterable)
else:
self.list = list()
self.lock = threading.Lock()
self.index = None
def append(self, x):
with self.lock:
self.list.append(x)
def __iter__(self):
shared_iterator = SharedList()
shared_iterator.list = self.list
shared_iterator.lock = self.lock
shared_iterator.index = 0
return shared_iterator
def next(self):
with self.lock:
if self.index < len(self.list):
result = self.list[self.index]
self.index += 1
else:
raise StopIteration
return result
# Override other methods
if __name__ == '__main__':
shared_list = SharedList()
for x in range(1, 4):
shared_list.append(x)
for entry in shared_list:
print entry
输出
1
2
3
正如 Georg Shölly 在评论中指出的那样,这将需要大量工作来实现每个方法。但是,如果您只需要一个可以追加然后迭代的列表,则此示例提供了起点。
那你就可以写
the_list = SharedList()
the_list.append("New Element")