3

我有设计问题:有一个全局资源不能同时从多个线程访问,所以我需要一个锁来序列化对它的访问。__del__但是,当我在持有锁的同时进行一些处理时,Python 的垃圾收集器可以运行一个方法。如果析构函数试图访问该资源,这将导致死锁。

例如,考虑以下看似无辜的单线程代码,如果你运行它会死锁:

import threading

class Handle(object):
    def __init__(self):
        self.handle = do_stuff("get")

    def close(self):
        h = self.handle
        self.handle = None
        if h is not None:
            do_stuff("close %d" % h)

    def __del__(self):
        self.close()

_resource_lock = threading.Lock()

def do_stuff(what):
    _resource_lock.acquire()
    try:
        # GC can be invoked here -> deadlock!
        for j in range(20):
            list()
        return 1234
    finally:
        _resource_lock.release()

for j in range(1000):
    xs = []
    b = Handle()
    xs.append(b)
    xs.append(xs)

该资源可以处理同时打开的多个“句柄”,我需要处理它们的生命周期。将其抽象到一个Handle类中并进行清理__del__似乎是一个聪明的举动,但上述问题打破了这一点。

处理清理的一种方法是保留句柄的“待清理”列表,如果在__del__运行时持有锁,则将句柄插入那里,稍后清理列表。

问题是:

  • 是否有gc.disable()/的线程安全版本gc.enable()可以以更清洁的方式解决此问题?

  • 其他想法如何处理?

4

2 回答 2

1

Python 的垃圾收集器不会清理具有“自定义”__del__方法的循环依赖项。

由于您已经有了一个__del__方法,您只需要一个循环依赖来“禁用”这些对象的 GC:

class Handle(object):
    def __init__(self):
        self.handle = do_stuff("get")
        self._self = self

现在,这会造成内存泄漏,那么我们该如何解决呢?

准备好释放对象后,只需删除循环依赖项:

import threading
import gc


class Handle(object):
    def __init__(self):
        self.handle = do_stuff("get")
        self._self = self

    def close(self):
        h = self.handle
        self.handle = None
        if h is not None:
            do_stuff("close %d" % h)

    def __del__(self):
        self.close()

_resource_lock = threading.Lock()

def do_stuff(what):
    _resource_lock.acquire()
    try:
        # GC can be invoked here -> deadlock!
        for j in range(20):
            list()
        return 1234
    finally:
        _resource_lock.release()

for j in range(1000):
    xs = []
    b = Handle()
    xs.append(b)
    xs.append(xs)


# Make sure the GC is up to date
gc.collect()
print "Length after work", len(gc.garbage)

# These are kept along due to our circular depency
# If we remove them from garbage, they come back
del gc.garbage[:]
gc.collect()
print "Length now", len(gc.garbage)

# Let's break it
for handle in gc.garbage:
    handle._self = None

# Now, our objects don't come back
del gc.garbage[:]
gc.collect()
print "Length after breaking circular dependencies", len(gc.garbage)

会做:

Length after work 999
Length now 999
Length after breaking circular dependencies 0

另一方面,为什么您需要在清理代码中访问这个复杂的库,而您无法控制其执行?

这里一个更干净的解决方案可能是在循环中进行清理,并在清理后打破循环依赖,以便 GC 可以做它的事情。

这是一个实现:

import threading
import gc


class Handle(object):
    def __init__(self):
        self.handle = do_stuff("get")
        self._self = self

    def close(self):
        h = self.handle
        self.handle = None
        if h is not None:
            do_stuff("close %d" % h)
        del self._self

    def __del__(self):
        # DO NOT TOUCH THIS
        self._ = None    

_resource_lock = threading.Lock()

def do_stuff(what):
    _resource_lock.acquire()
    try:
        # GC can be invoked here -> deadlock!
        for j in range(20):
            list()
        return 1234
    finally:
        _resource_lock.release()

for j in range(1000):
    xs = []
    b = Handle()
    xs.append(b)
    xs.append(xs)


# Make sure the GC is up to date
gc.collect()
print "Length after work", len(gc.garbage)

# These are kept along due to our circular depency
# If we remove them from garbage, they come back
del gc.garbage[:]
gc.collect()
print "Length now", len(gc.garbage)

# Let's break it
for handle in gc.garbage:
    handle.close()

# Now, our objects don't come back
del gc.garbage[:]
gc.collect()
print "Length after breaking circular dependencies", len(gc.garbage)

输出显示我们的循环依赖确实阻止了收集:

Length after work 999
Length now 999
Length after breaking circular dependencies 0
于 2013-09-12T21:34:29.110 回答
0

循环引用不是这个问题的关键。您可能有对象ab相互引用以形成一个圆圈,并用.a.resource指向一个对象。和被收集后(它们没有,所以它们被收集是安全的),被自动收集,并被调用。它可能发生在整个代码中,并且您无法控制它,因此它可能会产生死锁。c__del__ab__del__cc.__del__

还有其他没有引用计数的 Python 实现(例如 PyPy)。使用这些解释器,对象总是由 GC 收集。

唯一安全的使用方法__del__是在其中使用一些原子操作。锁不起作用:它们要么死锁(threading.Lock),要么永远不工作(threading.RLock)。由于追加到列表是 Python 中的原子操作,因此您可以将一些标志(或一些闭包)放入全局列表,并在其他线程中检查该列表以执行“真正的破坏”。

Python 3.7 中引入的新 GC 模式可能会解决问题https://www.python.org/dev/peps/pep-0556/

于 2018-06-04T07:32:15.383 回答