10.4.4. Código simplificado mediante guardas

El uso de mutexes se convierte rápidamente complicado cuando se introducen excepciones. Para estar seguro de que el mutes siempre se libera, debe asegurar que cualquier camino a una excepción incluya una llamada a release(). Además, cualquier función que tenga múltiples caminos para retornar debe asegurar cuidadosamente que se llama a release() en el momento adecuado.

Esos problemas pueden se fácilmente solucionados utilizando el hecho de que los objetos de la pila (automáticos) tiene un destructor que siempre se llama sea cual sea la forma en que salga del ámbito de la función. En la librería ZThread, esto se implementa en la plantilla Guard. La plantilla Guard crea objetos que adquieren un objeto Lockable cuando se construyen y lo liberan cuando son destruidos. Los objetos Guard creados en la pila local serán eliminados automáticamente independientemente de la forma en el que la función finalice y siempre desbloqueará el objeto Lockable. A continuación, el ejemplo anterior reimplementado para utilizar Guards:

//: C11:GuardedEvenGenerator.cpp {RunByHand}
// Simplifying mutexes with the Guard template.
//{L} ZThread
#include <iostream>
#include "EvenChecker.h"
#include "zthread/ThreadedExecutor.h"
#include "zthread/Mutex.h"
#include "zthread/Guard.h"
using namespace ZThread;
using namespace std;

class GuardedEvenGenerator : public Generator {
  unsigned int currentEvenValue;
  Mutex lock;
public:
  GuardedEvenGenerator() { currentEvenValue = 0; }
  ~GuardedEvenGenerator() {
    cout << "~GuardedEvenGenerator" << endl;
  }
  int nextValue() {
    Guard<Mutex> g(lock);
    ++currentEvenValue;
    Thread::yield();
    ++currentEvenValue;
    return currentEvenValue;
  }
};

int main() {
  EvenChecker::test<GuardedEvenGenerator>();
} ///:~

Listado 10.19. C11/GuardedEvenGenerator.cpp


Note que el valor de retorno temporal ya no es necesario en nextValue(). En general, hay menos código que escribir y la probabilidad de errores por parte del usuario se reduce en gran medida.

Una característica interesante de la plantilla Guard es que puede ser usada para manipular otros elementos de seguridad. Por ejemplo, un segundo Guard puede ser utilizado temporalmente para desbloquear un elemento de seguridad:

//: C11:TemporaryUnlocking.cpp
// Temporarily unlocking another guard.
//{L} ZThread
#include "zthread/Thread.h"
#include "zthread/Mutex.h"
#include "zthread/Guard.h"
using namespace ZThread;

class TemporaryUnlocking {
  Mutex lock;
public:
  void f() {
    Guard<Mutex> g(lock);
    // lock is acquired
    // ...
    {
      Guard<Mutex, UnlockedScope> h(g);
      // lock is released
      // ...
      // lock is acquired
    }
    // ...
    // lock is released
  }
};

int main() {
  TemporaryUnlocking t;
  t.f();
} ///:~

Listado 10.20. C11/TemporaryUnlocking.cpp


Un Guard también puede utilizarse para adquirir un lock durante un determinado tiempo y, después, liberarlo:

//: C11:TimedLocking.cpp
// Limited time locking.
//{L} ZThread
#include "zthread/Thread.h"
#include "zthread/Mutex.h"
#include "zthread/Guard.h"
using namespace ZThread;

class TimedLocking {
  Mutex lock;
public:
  void f() {
    Guard<Mutex, TimedLockedScope<500> > g(lock);
    // ...
  }
};

int main() {
  TimedLocking t;
  t.f();
} ///:~

Listado 10.21. C11/TimedLocking.cpp


En este ejemplo, se lanzará una Timeout_Exception si el lock no puede ser adquirido en 500 milisegundos.

Sincronización de clases completas

La librería ZThread también proporciona la plantilla GuardedClass para crear automáticamente un recubrimiento de sincronización para toda una clase. Esto quiere decir que cualquier método de una clase estará automáticamente protegido:

//: C11:SynchronizedClass.cpp {-dmc}
//{L} ZThread
#include "zthread/GuardedClass.h"
using namespace ZThread;

class MyClass {
public:
  void func1() {}
  void func2() {}
};

int main() {
  MyClass a;
  a.func1(); // Not synchronized
  a.func2(); // Not synchronized
  GuardedClass<MyClass> b(new MyClass);
  // Synchronized calls, only one thread at a time allowed:
  b->func1();
  b->func2();
} ///:~

Listado 10.22. C11/SynchronizedClass.cpp


El objeto a no está sincronizado, por lo que func1() y func2() pueden ser llamadas en cualquier momento por cualquier número de hilos. El objeto b está protegido por el recubrimiento GuardedClass, así que cada método se sincroniza automáticamente y solo se puede llamar a una función por objeto en cualquier instante.

El recubrimiento bloquea un tipo de nivel de granularidad, que podría afectar al rendimiento.[151] Si una clase contiene funciones no vinculadas, puede ser mejor sincronizarlas internamente con 2 locks diferentes. Sin embargo, si se encuentra haciendo esto, significa que la clase contiene grupos de datos que puede no estar fuertemente asociados. Considere dividir la clase en dos.

Proteger todos los métodos de una clase con un mutex no hace que esa clase sea segura automáticamente cuando se utilicen hilos. Debe tener cuidado con estas cuestiones para garantizar la seguridad cuando se usan hilos.