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 Guard
s:
//: 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.