10.4.3. Control de acceso

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.