quarta-feira, 19 de março de 2014

RLock

Dando continuidade o artigo sobre thread, um recurso muito útil é lock reentrante.

Lock reentrante é uma variação de lock que pode ser realocado múltiplas vezes pelo mesmo thread e não pode ser liberado por outro thread. É muito útil em funções recursivas, mas funciona também para garantir que o lock seja alocado e liberado pelo mesmo thread.

A fábrica (factory) para criar locks reentrantes é threading.RLock.

É preciso tomar cuidado para que todos os threads compartilhem o mesmo lock, senão ele se torna inútil:
lock = RLock()

thr1 = Thread(target=func1, args=(lock, ))
thr2 = Thread(target=func2, args=(lock, ))
thr3 = Thread(target=func3, args=(lock, ))

Dentro da função, é preciso alocá-lo (acquire) no inicío e liberá-lo (release) ao final. Por exemplo:
def func1(lock):
    lock.acquire()
    try:
        # executa o procedimento
        ...
    finally:
        lock.release()

Protegendo um objeto mutável

Uma utilidade para o lock reentrante é proteger métodos que alterem o conteúdo de um objeto.

Imagine que temos uma classe Person com dados, como identity_code (CPF) que podem sofrer alterações em threads diferentes (sei que não é uma boa abordagem, mas apenas como exemplo).

Podemos criar um decorador que torna um método thread-safe usando lock reentrante:
def lock(wrapped):
    lock_ = RLock()

    @wraps(wrapped)
    def wrapper(*args, **kwargs):
        with lock_:
            return wrapped(*args, **kwargs)

    return wrapper

Esse decorador pode ser usado nos setters de cada propriedade:
class Person(object):

    ...

    @property
    def identity_code(self):
        return self.__identity_code

    @identity_code.setter
    @lock
    def identity_code(self, value):
        self.__identity_code = value

    ...

Na verdade essa abordagem não resolve 100% o problema, mas já reduz muito a ocorrência de bugs.

Protegendo qualquer objeto

Porém a abordagem acima apenas protege parcialmente o objeto e não funciona para classes de terceiros.

Outra abordagem é usar um lock para todo o objeto, tanto leitura quanto gravação, agnóstico a qual objeto está sendo protegido. Assim, não há necessidade de usarmos propriedades.

Vamos criar uma classe para trancar qualquer objeto:
class ObjectLocker(object):

    def __init__(self, obj):
        self.__obj = obj
        self.__lock = RLock()

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

    def __exit__(self, etype, exc, traceback):
        self.__lock.release()

No código a instância dessa classe será passada para os threads, que terá de usar with para acessar o objeto original.

Ao usar with, o objeto original será trancado para o thread atual e liberado ao final.

A passagem será:
locker = ObjectLocker(Person(...))
thr = Thread(target=func, args=(locker, ))

Dentro da(s) função(ões) func a instância de Person deve ser acessa da seguinta forma:
with locker as person:
    name = person.name
    person.identity_code = data['identity_code']

Espero que os exemplos tenham sido úteis.

[]’s
Cacilhας, La Batalema

sábado, 15 de março de 2014

Thread-safe

Tenho tido a necessidade de lidar com muitas bibliotecas de terceiros e teno percebi um erro (ou seria uma abordagem?) comum nas mais novas: quase nenhuma delas é thread-safe.

Acredito que, com o modismo do uso de corrotinas (chamadas lightweight threads), os programadores mais novos passaram a considerar os threads de sistema obsoletos, deixando de tomar cuidados essenciais para boas bibliotecas.

Bem, tenho uma novidade para vocês: threads não são obsoletos e corrotinas não resolvem todos os problemas do mundo. Há situações em que usar corrotinas pode ser a melhor opção sim, mas em alguns casos os bons e velhos threads ainda são o salva vidas.

Para que isso seja possível, na criação de bibliotecas é necessário tomar alguns cuidados:
  • Prefira sempre que possível usar objetos imutáveis. Prefira tuplas, strings, tipos numéricos básicos, etc.
  • Evite permitir que objetos agregados façam alteração em seu objeto contentor sempre que possível.
  • Em todos métodos e propriedades de um objeto que pode ser compartilhado (como o contentor citado) que alterem o estado do objeto, inicie com um RLock, chamando seu método acquire, e encerre chamando seu método release. Tome cuidado para que seja usada a mesma instância de RLock!
  • Não tenha medo de usar objetos do módulo threading: Condition, Event, Lock, RLock e BoundedSemaphore. Eles são seus amigos. ;-)
  • Se estiver difícil escrever testes, substitua threading por dummy_threading para os testes unitários, mas use threading para testes de aceitação.

[]’s
Cacilhας, La Batalema