En el ejemplo anterior se muestra el problema fundamental a la hora de utilizar hilos: nunca sabrá cuándo un hilo puede ser ejecutado. Imagínese sentado en la mesa con un tenedor, a punto de coger el último pedazo de comida de un plato y tan pronto como su tenedor lo alcanza, de repente, la comida se desvanece (debido a que su hilo fue suspendido y otro vino y se comió la comida). Ese es el problema que estamos tratando a la hora de escribir programas concurrentes.
En ocasiones no le importará si un recurso está siendo accedido a la vez que intenta usarlo. Pero en la mayoría de los casos sí, y para trabajar con múltiples hilos necesitará alguna forma de evitar que dos hilos accedan al mismo recurso, al menos durante períodos críticos.
Para prevenir este tipo de colisiones existe una manera sencilla que consiste en poner un bloqueo sobre un recursos cuando un hilo trata de usarlo. El primer hilo que accede al recursos lo bloquea y, así, otro hilo no puede acceder al recurso hasta que no sea desbloqueado, momento en el que este hilo lo vuelve a bloquear y lo vuelve a usar, y así sucesivamente.
De esta forma, tenemos que ser capaces de evitar cualquier tarea de acceso a memoria mientras ese almacenamiento no esté en un estado adecuado. Esto es, necesitamos tener un mecanismo que excluya una segunda tarea sobre el acceso a memoria cuando una primera tarea ya está usándola. Esta idea es fundamental para todo sistema multihilado y se conoce como exclusión mutua; abreviado como mutex. La biblioteca ZThread tiene un mecanismo de mutex en el fichero de cabecera Mutex.h.
Para solucionar el problema en el programa anterior, identificaremos las secciones críticas donde debe aplicarse la exclusión mutua; posteriormente, antes de entrar en la sección crítica adquiriremos el mutex y lo liberaremos cuando finalice la sección crítica. Únicamente un hilo podrá adquirir el mutex al mismo tiempo, por lo que se logra exclusión mutua:
//: C11:MutexEvenGenerator.cpp {RunByHand} // Preventing thread collisions with mutexes. //{L} ZThread #include <iostream> #include "EvenChecker.h" #include "zthread/ThreadedExecutor.h" #include "zthread/Mutex.h" using namespace ZThread; using namespace std; class MutexEvenGenerator : public Generator { unsigned int currentEvenValue; Mutex lock; public: MutexEvenGenerator() { currentEvenValue = 0; } ~MutexEvenGenerator() { cout << "~MutexEvenGenerator" << endl; } int nextValue() { lock.acquire(); ++currentEvenValue; Thread::yield(); // Cause failure faster ++currentEvenValue; int rval = currentEvenValue; lock.release(); return rval; } }; int main() { EvenChecker::test<MutexEvenGenerator>(); } ///:~
Listado 10.18. C11/MutexEvenGenerator.cpp
MutexEvenGenerator
añade
un Mutex
llamado lock y utiliza
acquire() y release() para crear una sección crítica con
nextValue(). Además, se ha insertado una llamada a
Thread::yield() entre los dos incrementos, para aumentar la
probabilidad de que haya un cambio de contexto mientras
currentEvenValue se encuentra en un estado extraño. Este hecho
no producirá un fallo ya que el mutex evita que más de un hilo
esté en la sección crítica al mismo tiempo, pero llamar a
yield() es una buena forma de provocar un fallo si este
ocurriera.
Note que nextValue() debe capturar el valor de retorno dentro de la sección crítica porque si lo devolviera dentro de la sección critica no liberaría lock y así evitar que fuera adquirido. (Normalmente, esto conllevaría un interbloqueo, de lo cual aprenderá sobre ello al final de este capítulo.)
El primer hilo que entre en nextValue() adquirirá lock y cualquier otro hilo que intente adquirirlo será bloqueado hasta que el primer hilo libere lock. En ese momento, el mecanismo de planificación selecciona otro hilo que esté esperando en lock. De esta manera, solo un hilo puede pasar a través del código custodiado por el mutex al mismo tiempo.