6

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

state = atomicObject(dict())

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

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

编辑:如何使内置容器(集合、字典、列表)线程安全?. 然而; 正如 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)

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

4

5 回答 5

4

从您的非运行示例和乱七八糟的代码中很难分辨eval,但至少有一个明显的错误。

在您的交互式解释器中试试这个:

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

正如文档所说,ismethod

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

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

你可能只是想要callableinspect.isroutine在这里。

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

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

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


你真的不应该试图创造和eval任何东西。要按名称动态分配属性,请使用setattr. 而且你不需要复杂lambda的 s。只需用普通定义包装函数def;结果是一个非常好的本地值,您可以传递它,就像 a 一样,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

问题是,正如文档所暗示的那样,在 class 上查找特殊方法,而不是在 object 上。而且这个atomicObject类没有__setitem__方法。

事实上,这意味着您甚至无法有效地打印出您的对象,因为您只是获得了默认值__str____repr__from 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

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

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

尝试运行此示例。通过适当的锁定,它应该总是打印出 2000。但在我的笔记本电脑上,它通常接近 125。

于 2013-04-12T00:29:36.837 回答
2

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

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

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

但是您宁愿编写您的程序以使用各种可用框架之一来使用异步调用,或者为了轻松利用多个内核,使用multiprocessing而不是threading- 这基本上为每个“线程”创建一个进程 - 并且需要所有共享都明确完成。

于 2013-04-12T01:45:03.067 回答
1

然后 wrapt 模块包含那里描述的 @synchronized 装饰器。

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

可以在以下位置找到描述装饰器及其工作原理的演讲:

于 2019-05-31T02:15:25.890 回答
0

尽管我的其他答案 - 它对 Python 线程有有效的考虑,并将现有对象转换为“原子”锁定对象 - 如果您正在定义要以原子方式锁定的对象的类,那么整个事情就是一个顺序量级更简单。

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

下面的代码适用于 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)

注意:虽然“eval”和“exec”在 Python 中可用,但严肃的代码很少——我的意思是很少——需要任何一个。即使是重新创建函数的复杂装饰器也可以通过自省来完成,而不是通过 eval 依赖字符串编译。

于 2013-04-12T02:25:07.093 回答
0

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

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)
于 2016-06-03T05:37:06.670 回答