如何使用互斥锁装饰Python对象

ara*_*ore 6 python locking wrapper

我是python的新手,目前正在尝试学习线程.我厌倦了使用锁来使我的资源成为线程安全的,因为它们本身并不依赖于资源,所以每次我的代码与资源交互时,我都会忘记获取和/或释放它们.相反,我希望能够"包装"(或装饰?)一个对象,以便它的所有方法和属性getter/setter都是原子的.这样的事情:

state = atomicObject(dict())

# the following is atomic/thread-safe
state["some key"] = "some value"
Run Code Online (Sandbox Code Playgroud)

这可能吗?如果是这样,那么实施它的"最佳实践"方式是什么?

编辑:如何使内置容器(集,dicts,列表)线程安全可用的上述问题的一个很好的答案?.然而; 正如abarnert和jsbueno都证明的那样,我提出的解决方案(自动锁定)通常不是一个好主意,因为确定原子操作的适当粒度需要一些智能,并且可能很难(或不可能)正确自动化.

问题仍然存在,锁不以任何方式绑定到它们要保护的资源,因此我的新问题是:将锁与对象关联起来的好方法是什么?

提出的解决方案#2:我想可能有一种方法可以将锁绑定到一个对象,这样在没有首先获取锁的情况下尝试访问该对象会引发错误,但我可以看到这可能会变得棘手.

编辑:以下代码与问题无关.我发布它是为了证明我曾尝试自己解决问题并在发布此问题之前迷路了.

为了记录,我编写了以下代码,但它不起作用:

import threading    
import types
import inspect

class atomicObject(object):

    def __init__(self, obj):
        self.lock = threading.RLock()
        self.obj = obj

        # keep track of function handles for lambda functions that will be created
        self.funcs = []

        # loop through all the attributes of the passed in object
        # and create wrapped versions of each attribute
        for name in dir(self.obj):
            value = getattr(self.obj, name)
            if inspect.ismethod(value):
                # this is where things get really ugly as i try to work around the
                # limitations of lambda functions and use eval()... I'm not proud of this code
                eval("self.funcs.append(lambda self, *args, **kwargs: self.obj." + name + "(*args, **kwargs))")
                fidx = str(len(self.funcs) - 1)
                eval("self." + name + " = types.MethodType(lambda self, *args, **kwargs: self.atomize(" + fidx + ", *args, **kwargs), self)")

    def atomize(self, fidx, *args, **kwargs):
        with self.lock:
            return self.functions[fidx](*args, **kwargs)
Run Code Online (Sandbox Code Playgroud)

我可以创建一个atomicObject(dict()),但是当我尝试向对象添加一个值时,我得到了错误; "atomicObject不支持项目分配".

aba*_*ert 4

很难从未运行的示例和混乱的eval代码中辨别出来,但至少有一个明显的错误。

\n\n

在交互式解释器中尝试一下:

\n\n
>>> d = dict()\n>>> inspect.ismethod(d.__setitem__)\n
Run Code Online (Sandbox Code Playgroud)\n\n

正如文档所说,ismethod:

\n\n
\n

如果对象是用 Python 编写的绑定方法,则返回 true。

\n
\n\n

用 C(或 .NET、Java、其他 Python 实现的下一个工作区等)编写的方法包装器不是用 Python 编写的绑定方法。

\n\n

你可能只是想要callable或inspect.isroutine在这里。

\n\n

我不能说这是否是唯一的问题,因为如果我修复语法错误和名称错误以及这个错误,第二行eval会生成如下非法代码:

\n\n
self.__cmp__ = types.MethodType(lambda self, *args, **kwargs: self.atomize(0, *args, **kwargs) self)\n
Run Code Online (Sandbox Code Playgroud)\n\n

\xe2\x80\xa6 ,我不确定你想在那里做什么。

\n\n
\n\n

你真的不应该尝试去创造eval什么。要按名称动态分配属性,请使用setattr. 而且你不需要复杂的lambda。只需用普通的 ; 定义包装函数即可def。结果是一个完美的本地值,您可以传递它,就像 a 一样,lambda只是它有一个名称。

\n\n

最重要的是,尝试在创建时静态包装方法很困难,并且有一些重大缺点。(例如,如果您要包装的类具有任何动态生成的方法,则您不会包装它们。)大多数时候,您最好在调用时使用__getattr__. (如果您担心每次调用包装函数时创建包装函数的成本\xe2\x80\xa6\xc2\xa0首先,请不要担心,除非您实际分析并发现它是一个瓶颈,因为它可能不会。但是,如果是,您可以轻松添加生成函数的缓存。)

\n\n

因此,这是我认为您正在尝试做的事情的更简单且有效的实现:

\n\n
class atomicObject(object):\n\n    def __init__(self, obj):\n        self.lock = threading.Lock()\n        self.obj = obj\n\n    def __getattr__(self, name):\n        attr = getattr(self.obj, name)\n        print(attr)\n        if callable(attr):\n            def atomized(*args, **kwargs):\n                with self.lock:\n                    attr(*args, **kwargs)\n            return atomized\n        return attr\n
Run Code Online (Sandbox Code Playgroud)\n\n

但是,这实际上并不会达到您想要的效果。例如:

\n\n
>>> d = atomicObject(dict())\n>>> d.update({\'a\': 4}) # works\n>>> d[\'b\'] = 5\nTypeError: \'atomicObject\' object does not support item assignment\n
Run Code Online (Sandbox Code Playgroud)\n\n

为什么会出现这种情况?你已经有了一个__setitem__,它可以工作:

\n\n
>>> d.__setitem__\n<method-wrapper \'__setitem__\' of dict object at 0x100706830>\n>>> d.__setitem__(\'b\', 5) # works\n
Run Code Online (Sandbox Code Playgroud)\n\n

问题在于,正如文档所暗示的那样,特殊方法是在类上查找的,而不是在对象上查找的。并且该类atomicObject没有__setitem__方法。

\n\n

事实上,这意味着您甚至无法有效地打印出您的对象,因为您只是从以下位置获得默认值__str__和:__repr__object

\n\n
>>> d\n<__main__.atomicObject object at 0x100714690>\n>>> print(d)\n<__main__.atomicObject object at 0x100714690>\n>>> d.obj #cheating\n{\'a\': 4, \'b\': 5}\n
Run Code Online (Sandbox Code Playgroud)\n\n
\n\n

因此,这里正确的做法是编写一个为任何类定义包装类的函数,然后执行以下操作:

\n\n
>>> AtomicDict = make_atomic_wrapper(dict)\n>>> d = AtomicDict()\n
Run Code Online (Sandbox Code Playgroud)\n\n
\n\n

但是,即使您完成了所有这些\xe2\x80\xa6,这也很少像听起来那么好。

\n\n

考虑一下:

\n\n
d = AtomicDict()\nd[\'abc\'] = 0\nd[\'abc\'] += 1\n
Run Code Online (Sandbox Code Playgroud)\n\n

最后一行不是原子的。有一个atomic __getitem__,然后是一个单独的atomic __setitem__。

\n\n

这听起来可能没什么大不了的,但想象一下它d被用作计数器。您有 20 个线程试图d[\'abc\'] += 1同时执行。第一个加入的人__getitem__将会回来0。如果它是最后一个进入 的__setitem__,它会将其设置为1。

\n\n

尝试运行这个例子。如果锁定正确,它应该始终打印出 2000。但在我的笔记本电脑上,它通常更接近 125。

\n