¿Cómo hacer que los contenedores incorporados (conjuntos, diccionarios, listas) sean seguros?

Entiendo de esta pregunta que si quiero tener un set que sea seguro para subprocesos, tengo que implementar la parte de seguridad de subprocesos por mi cuenta.

Por lo tanto podría llegar a:

 from threading import Lock class LockedSet(set): """A set where add() and remove() are thread-safe""" def __init__(self, *args, **kwargs): # Create a lock self._lock = Lock() # Call the original __init__ super(LockedSet, self).__init__(*args, **kwargs) def add(self, elem): self._lock.acquire() try: super(LockedSet, self).add(elem) finally: self._lock.release() def remove(self, elem): self._lock.acquire() try: super(LockedSet, self).remove(elem) finally: self._lock.release() 

Entonces, por supuesto, solo add () y remove () son seguros para subprocesos en esta implementación. Los otros métodos no son porque no se sobrescribieron en la subclase.

Ahora, el patrón es bastante simple: adquirir locking, llamar al método original, liberar locking. Si sigo la lógica anterior, tendría que sobrescribir todos los métodos expuestos por set esencialmente de la misma manera, por ejemplo:

(pseudocódigo)

 def (): 1. acquire lock 2. try: 3. call original method passing  4. finally: 5. release lock 

(/ pseudo-codigo)

Esto no solo es tedioso sino también propenso a errores. Entonces, ¿alguna idea / sugerencia sobre cómo abordar esto de una mejor manera?

Related of "¿Cómo hacer que los contenedores incorporados (conjuntos, diccionarios, listas) sean seguros?"

Puedes usar las funciones de metaprogtwigción de Python para lograr esto. (Nota: escrito rápidamente y no probado a fondo). Prefiero usar un decorador de clase.

También creo que es posible que deba bloquear más que add y remove para que un conjunto sea seguro para subprocesos, pero no estoy seguro. Ignoraré ese problema y me concentraré en tu pregunta.

También considere si la delegación (proxy) es mejor que la subclasificación. Envolver objetos es el enfoque habitual en Python.

Finalmente, no hay una “varita mágica” de metaprogtwigción que mágicamente agregue un locking de grano fino a cualquier colección de Python mutable. Lo más seguro es bloquear el acceso a cualquier método o atributo mediante RLock , pero esto es muy RLock y lento, y probablemente aún no sea una garantía de que su objeto estará seguro para subprocesos en todos los casos. (Por ejemplo, puede tener una colección que manipula otro objeto no seguro para subprocesos accesible a otros subprocesos). Realmente necesita examinar todas y cada una de las estructuras de datos y pensar qué operaciones son atómicas o requieren lockings y qué métodos pueden llamar otros métodos. utilizando el mismo locking (es decir, el propio interlocking).

Dicho esto, aquí hay algunas técnicas a su disposición en orden creciente de abstracción:

Delegación

 class LockProxy(object): def __init__(self, obj): self.__obj = obj self.__lock = RLock() # RLock because object methods may call own methods def __getattr__(self, name): def wrapped(*a, **k): with self.__lock: getattr(self.__obj, name)(*a, **k) return wrapped lockedset = LockProxy(set([1,2,3])) 

Gestor de contexto

 class LockedSet(set): """A set where add(), remove(), and 'in' operator are thread-safe""" def __init__(self, *args, **kwargs): self._lock = Lock() super(LockedSet, self).__init__(*args, **kwargs) def add(self, elem): with self._lock: super(LockedSet, self).add(elem) def remove(self, elem): with self._lock: super(LockedSet, self).remove(elem) def __contains__(self, elem): with self._lock: super(LockedSet, self).__contains__(elem) 

Decorador

 def locked_method(method): """Method decorator. Requires a lock object at self._lock""" def newmethod(self, *args, **kwargs): with self._lock: return method(self, *args, **kwargs) return newmethod class DecoratorLockedSet(set): def __init__(self, *args, **kwargs): self._lock = Lock() super(DecoratorLockedSet, self).__init__(*args, **kwargs) @locked_method def add(self, *args, **kwargs): return super(DecoratorLockedSet, self).add(elem) @locked_method def remove(self, *args, **kwargs): return super(DecoratorLockedSet, self).remove(elem) 

Decorador de clase

Creo que este es el método más limpio y fácil de entender de los métodos abstractos, por lo que lo he ampliado para permitir que se especifiquen los métodos para bloquear y bloquear una fábrica de objetos.

 def lock_class(methodnames, lockfactory): return lambda cls: make_threadsafe(cls, methodnames, lockfactory) def lock_method(method): if getattr(method, '__is_locked', False): raise TypeError("Method %r is already locked!" % method) def locked_method(self, *arg, **kwarg): with self._lock: return method(self, *arg, **kwarg) locked_method.__name__ = '%s(%s)' % ('lock_method', method.__name__) locked_method.__is_locked = True return locked_method def make_threadsafe(cls, methodnames, lockfactory): init = cls.__init__ def newinit(self, *arg, **kwarg): init(self, *arg, **kwarg) self._lock = lockfactory() cls.__init__ = newinit for methodname in methodnames: oldmethod = getattr(cls, methodname) newmethod = lock_method(oldmethod) setattr(cls, methodname, newmethod) return cls @lock_class(['add','remove'], Lock) class ClassDecoratorLockedSet(set): @lock_method # if you double-lock a method, a TypeError is raised def frobnify(self): pass 

Anular el acceso a los atributos con __getattribute__

 class AttrLockedSet(set): def __init__(self, *args, **kwargs): self._lock = Lock() super(AttrLockedSet, self).__init__(*args, **kwargs) def __getattribute__(self, name): if name in ['add','remove']: # note: makes a new callable object "lockedmethod" on every call # best to add a layer of memoization lock = self._lock def lockedmethod(*args, **kwargs): with lock: return super(AttrLockedSet, self).__getattribute__(name)(*args, **kwargs) return lockedmethod else: return super(AttrLockedSet, self).__getattribute__(name) 

Métodos de envoltura dinámicamente agregados con __new__

 class NewLockedSet(set): def __new__(cls, *args, **kwargs): # modify the class by adding new unbound methods # you could also attach a single __getattribute__ like above for membername in ['add', 'remove']: def scoper(membername=membername): # You can also return the function or use a class def lockedmethod(self, *args, **kwargs): with self._lock: m = getattr(super(NewLockedSet, self), membername) return m(*args, **kwargs) lockedmethod.__name__ = membername setattr(cls, membername, lockedmethod) self = super(NewLockedSet, cls).__new__(cls, *args, **kwargs) self._lock = Lock() return self 

Métodos de envoltura agregados dinámicamente con __metaclass__

 def _lockname(classname): return '_%s__%s' % (classname, 'lock') class LockedClass(type): def __new__(mcls, name, bases, dict_): # we'll bind these after we add the methods cls = None def lockmethodfactory(methodname, lockattr): def lockedmethod(self, *args, **kwargs): with getattr(self, lockattr): m = getattr(super(cls, self), methodname) return m(*args,**kwargs) lockedmethod.__name__ = methodname return lockedmethod lockattr = _lockname(name) for methodname in ['add','remove']: dict_[methodname] = lockmethodfactory(methodname, lockattr) cls = type.__new__(mcls, name, bases, dict_) return cls def __call__(self, *args, **kwargs): #self is a class--ie an "instance" of the LockedClass type instance = super(LockedClass, self).__call__(*args, **kwargs) setattr(instance, _lockname(self.__name__), Lock()) return instance class MetaLockedSet(set): __metaclass__ = LockedClass 

Metaclases creadas dinámicamente

 def LockedClassMetaFactory(wrapmethods): class LockedClass(type): def __new__(mcls, name, bases, dict_): # we'll bind these after we add the methods cls = None def lockmethodfactory(methodname, lockattr): def lockedmethod(self, *args, **kwargs): with getattr(self, lockattr): m = getattr(super(cls, self), methodname) return m(*args,**kwargs) lockedmethod.__name__ = methodname return lockedmethod lockattr = _lockname(name) for methodname in wrapmethods: dict_[methodname] = lockmethodfactory(methodname, lockattr) cls = type.__new__(mcls, name, bases, dict_) return cls def __call__(self, *args, **kwargs): #self is a class--ie an "instance" of the LockedClass type instance = super(LockedClass, self).__call__(*args, **kwargs) setattr(instance, _lockname(self.__name__), Lock()) return instance return LockedClass class MetaFactoryLockedSet(set): __metaclass__ = LockedClassMetaFactory(['add','remove']) 

Apostaré usando un try...finally simple y explícito try...finally no se ve tan mal ahora, ¿verdad?

Ejercicio para el lector: deje que la persona que llama pase su propio objeto Lock() (dependency injection) utilizando cualquiera de estos métodos.

Este es mi primer bash de jugar con decoradores (aunque mi código no usa realmente la syntax de @decorate), y no tengo mucha experiencia con multiproceso / multiproceso. Sin embargo, con ese descargo de responsabilidad, aquí hay un bash que hice:

 from multiprocessing import Lock def decorate_all(obj): lock = Lock() #you'll want to make this more robust: fnc_names = [fnctn for fnctn in dir(obj) if '__' not in fnctn] for name in fnc_names: print 'decorating ' + name fnc = getattr(obj, name) setattr(obj, name, decorate(fnc, lock)) return obj def decorate(fnctn, lock): def decorated(*args): print 'acquiring lock' lock.acquire() try: print 'calling decorated function' return fnctn(*args) finally: print 'releasing lock' lock.release() return decorated def thread_safe(superclass): lock = Lock() class Thread_Safe(superclass): def __init__(self, *args, **kwargs): super(Thread_Safe, self).__init__(*args, **kwargs) return decorate_all(Thread_Safe) >>> thread_safe_set = thread_safe(set) decorating add decorating clear decorating copy decorating difference decorating difference_update decorating discard decorating intersection decorating intersection_update decorating isdisjoint decorating issubset decorating issuperset decorating pop decorating remove decorating symmetric_difference decorating symmetric_difference_update decorating union decorating update >>> s = thread_safe_set() >>> s.add(1) acquiring lock calling decorated function releasing lock >>> s.add(4) acquiring lock calling decorated function releasing lock >>> s.pop() acquiring lock calling decorated function releasing lock 1 >>> s.pop() acquiring lock calling decorated function releasing lock 4 >>> 

[De hecho, ver los comentarios, no es cierto]

Si está ejecutando CPython, puede ver en el código fuente del conjunto que no libera GIL (http://hg.python.org/cpython/file/db20367b20de/Objects/setobject.c), por lo que todas sus operaciones deberían ser atómico.

Si es todo lo que necesita y está seguro de ejecutar su código en CPython, puede usarlo directamente.

Puedes implementar tu propio administrador de contexto:

 class LockableSet: def __enter__(self): self.lock() return self def __exit__(self, exc_type, exc_value, traceback): #Do what you want with the error self.unlock() with LockableSet() as s: s.whatever() raise Exception() 

__exit__ que __exit__ método __exit__ del objeto se llamará al final. Más información detallada está disponible aquí (documentos oficiales de python).

Otro uso para esto podría ser un decorador de lock para métodos, como este:

 def lock(func): def safe_func(self, *args, **kwargs): with self: func(self, *args, **kwargs) return safe_func