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

时间:2013-04-11 23:56:34

标签: python locking wrapper

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

state = atomicObject(dict())

# the following is atomic/thread-safe
state["some key"] = "some value"

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

编辑:How to make built-in containers (sets, dicts, lists) thread safe?提供了对上述问题的一个很好的答案。然而;正如abarnert和jsbueno都证明的那样,我提出的解决方案(自动锁定)通常不是一个好主意,因为确定原子操作的适当粒度需要一些智能,并且可能很难(或不可能)正确自动化。

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

建议的解决方案#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)

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

5 个答案:

答案 0 :(得分:4)

很难从你的非运行示例和你的eval代码中弄出来,但至少有一个明显的错误。

在交互式口译员中尝试此操作:

>>> d = dict()
>>> inspect.ismethod(d.__setitem__)

正如the docs所说,ismethod

  

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

用C语言编写的方法包装器(或Java,下一个工作空间等,用于其他Python实现)不是用Python编写的绑定方法。

您可能只想在这里callableinspect.isroutine

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

self.__cmp__ = types.MethodType(lambda self, *args, **kwargs: self.atomize(0, *args, **kwargs) self)

......我不确定你在那里做什么。


你真的不应该尝试创建和eval任何东西。要按名称动态分配属性,请使用setattr。而且你不需要复杂的lambda。只需使用普通def定义包装函数;结果是一个非常好的本地值,你可以传递,完全像lambda,除了它有一个名字。

最重要的是,尝试在创建时静态包装方法很困难,并且有一些主要的缺点。 (例如,如果您要包装的类具有任何动态生成的方法,则不会将它们包装起来。)大多数情况下,您最好在调用时动态地使用__getattr__ 。 (如果你担心每次调用它们时都会产生包装函数的成本......首先,不要担心,除非你真正描述它并发现它是一个瓶颈,因为它可能不会。但是,如果它是的,您可以轻松添加生成函数的缓存。)

所以,这是一个更简单,更有效的实现我认为你想要做的事情:

class atomicObject(object):

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

    def __getattr__(self, name):
        attr = getattr(self.obj, name)
        print(attr)
        if callable(attr):
            def atomized(*args, **kwargs):
                with self.lock:
                    attr(*args, **kwargs)
            return atomized
        return attr

然而,这实际上并不是你想要的。例如:

>>> d = atomicObject(dict())
>>> d.update({'a': 4}) # works
>>> d['b'] = 5
TypeError: 'atomicObject' object does not support item assignment

为什么会这样?你有__setitem__,它有效:

>>> d.__setitem__
<method-wrapper '__setitem__' of dict object at 0x100706830>
>>> d.__setitem__('b', 5) # works

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

事实上,这意味着您甚至无法打印出自己的对象,因为您只能从__str__获得默认__repr__object

>>> d
<__main__.atomicObject object at 0x100714690>
>>> print(d)
<__main__.atomicObject object at 0x100714690>
>>> d.obj #cheating
{'a': 4, 'b': 5}

所以,这里要做的正确的事情是编写一个为任何类定义包装类的函数,然后执行:

>>> AtomicDict = make_atomic_wrapper(dict)
>>> d = AtomicDict()

但是,即使你完成了所有这些......这听起来也不是一个好主意。

考虑一下:

d = AtomicDict()
d['abc'] = 0
d['abc'] += 1

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

这可能听起来不是什么大事,但想象d被用作反击。你有20个线程都试图同时做d['abc'] += 1。第一个进入__getitem__的人将返回0。如果它是最后一个进入__setitem__的人,则会将其设置为1

尝试运行this example。通过适当的锁定,它应该始终打印出2000.但在我的笔记本电脑上,它通常接近125。

答案 1 :(得分:2)

我已经对你的问题进行了一些思考,这将是一件非常棘手的事情 - 你不仅要使用Atomic类代理所有对象方法,而且可以正确地编写__getattribute__方法 - 但是对于运算符本身来说,你还必须为代理对象提供一个类,它提供与原始对象类相同的“魔术双下划线”方法 - 也就是说,你必须动态创建一个代理类 - 否则运算符用法本身不会是原子的。

可行 - 但由于您是Python的新手,您可以在交互式提示符上执行import this,在显示的几个指南/建议中,您将看到: “”如果实施很难解释,这是一个坏主意。“”“: - )

这将我们带到: 在Python中使用线程通常是一个坏主意。除了具有大量阻塞I / O的准平凡代码之外 - 你会更喜欢另一种方法 - 例如Python中的线程不允许普通的Python代码使用更多的CPU内核 - 例如,只有一个Python代码线程立即运行 - 搜索“Python GIL”以了解原因 - (例外,如果您的许多代码花费在计算密集型本机代码中,例如Numpy函数)。

但是你宁愿写一个程序来使用各种可用框架之一来使用异步调用,或者为了轻松利用多个核心,使用multiprocessing代替threading - 基本上每个“线程”创建一个进程 - 并且要求所有共享都明确地完成。

答案 2 :(得分:0)

尽管我的另一个答案 - 对Python线程有正确的考虑,并且现有的对象转换为“原子”锁定对象 - 如果你定义的话你想要原子地锁定的对象的类,整个事情比一个数量级更简单。

可以使用函数修饰器来使函数以带有四行的锁运行。有了它,就可以构建一个类装饰器,它以原子方式锁定给定类的所有方法和属性。

代码bellow适用于Python 2和3(我使用了@ abarnet的函数调用示例 - 并依赖于我的“打印调试”来获取类示例。)

import threading
from functools import wraps

#see http://stackoverflow.com/questions/15960881/how-to-decorate-a-python-object-with-a-mutex/15961762#15960881

printing = False

lock = threading.Lock()
def atomize(func):
    @wraps(func)
    def wrapper(*args, **kw):
        with lock:
            if printing:
                print ("atomic")
            return func(*args, **kw)
    return wrapper

def Atomic(cls):
    new_dict = {}
    for key, value in cls.__dict__.items():
        if hasattr(value, "__get__"):
            def get_atomic_descriptor(desc):
                class Descriptor(object):
                    @atomize
                    def __get__(self, instance, owner):
                        return desc.__get__(instance, owner)
                    if hasattr(desc, "__set__"):
                        @atomize
                        def __set__(self, instance, value):
                            return desc.__set__(instance, value)
                    if hasattr(desc, "__delete__"):
                        @atomize
                        def __delete__(self, instance):
                            return desc.__delete__(instance)
                return Descriptor()
            new_dict[key] = get_atomic_descriptor(value)
        elif callable(value):
            new_dict[key] = atomize(value)
        else:
            new_dict[key] = value
    return type.__new__(cls.__class__, cls.__name__, cls.__bases__, new_dict)


if __name__ == "__main__": # demo:
    printing = True

    @atomize
    def sum(a,b):
        return a + b

    print (sum(2,3))

    @Atomic
    class MyObject(object):
        def _get_a(self):
            return self.__a

        def _set_a(self, value):
            self.__a = value +  1

        a = property(_get_a, _set_a)

        def smurf(self, b):
            return self.a + b

    x = MyObject()
    x.a = 5
    print(x.a)
    print (x.smurf(10))

    # example of atomized function call - based on
    # @abarnet's code at http://pastebin.com/MrtR6Ufh
    import time, random
    printing = False
    x = 0

    def incr():
        global x
        for i in range(100):
            xx = x
            xx += 1
            time.sleep(random.uniform(0, 0.02))
            x = xx

    def do_it():
        threads = [threading.Thread(target=incr) for _ in range(20)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

    do_it()
    print("Unlocked Run: ", x)

    x = 0
    incr = atomize(incr)
    do_it()
    print("Locked Run: ", x)

注意:虽然Python中提供了“eval”和“exec”,但严重的代码很少 - 我的意思是很少 - 也需要。即使是复制函数的复杂装饰器也可以通过内省而不是依赖于通过eval进行字符串编译。

答案 3 :(得分:0)

回到这几年之后。我认为上下文管理器是我原始问题的理想解决方案。我知道Locks支持上下文管理,但是你仍然存在强制执行锁和锁定资源之间关系的问题。相反,我想像下面这样的东西会很好:

class Locked:
    def __init__(self, obj):
        super().__init__()
        self.__obj = obj
        self.lock = threading.RLock()

    def __enter__(self):
        self.lock.acquire()
        return self.__obj

    def __exit__(self, *args, **kwargs):
        self.lock.release()


guard = Locked(dict())

with guard as resource:
    do_things_with(resource)

答案 4 :(得分:0)

然后,包装模块包含此处描述的@synchronized装饰器。

https://pypi.python.org/pypi/wrapt