10.5.2. El jardín ornamental

En esta simulación, al comité del jardín le gustaría saber cuanta gente entra en el jardín cada día a través de distintas puertas. Cada puerta tiene un FIXMEturnstile o algún otro tipo de contador, y después de que el contador FIXMEturnstile se incrementa, aumenta una cuenta compartida que representa el número total de gente en el jardín.

//: C11:OrnamentalGarden.cpp {RunByHand}
//{L} ZThread
#include <vector>
#include <cstdlib>
#include <ctime>
#include "Display.h"
#include "zthread/Thread.h"
#include "zthread/FastMutex.h"
#include "zthread/Guard.h"
#include "zthread/ThreadedExecutor.h"
#include "zthread/CountedPtr.h"
using namespace ZThread;
using namespace std;

class Count : public Cancelable {
  FastMutex lock;
  int count;
  bool paused, canceled;
public:
  Count() : count(0), paused(false), canceled(false) {}
  int increment() {
    // Comment the following line to see counting fail:
    Guard<FastMutex> g(lock);
    int temp = count ;
    if(rand() % 2 == 0) // Yield half the time
      Thread::yield();
    return (count  = ++temp);
  }
  int value() {
    Guard<FastMutex> g(lock);
    return count;
  }
  void cancel() {
    Guard<FastMutex> g(lock);
    canceled = true;
  }
  bool isCanceled() {
    Guard<FastMutex> g(lock);
    return canceled;
  }
  void pause() {
    Guard<FastMutex> g(lock);
    paused = true;
  }
  bool isPaused() {
    Guard<FastMutex> g(lock);
    return paused;
  }
};

class Entrance : public Runnable {
  CountedPtr<Count> count;
  CountedPtr<Display> display;
  int number;
  int id;
  bool waitingForCancel;
public:
  Entrance(CountedPtr<Count>& cnt,
    CountedPtr<Display>& disp, int idn)
  : count(cnt), display(disp), number(0), id(idn),
    waitingForCancel(false) {}
  void run() {
    while(!count->isPaused()) {
      ++number;
      {
        ostringstream os;
        os << *this << " Total: "
           << count->increment() << endl;
        display->output(os);
      }
      Thread::sleep(100);
    }
    waitingForCancel = true;
    while(!count->isCanceled()) // Hold here...
      Thread::sleep(100);
    ostringstream os;
    os << "Terminating " << *this << endl;
    display->output(os);
  }
  int getValue() {
    while(count->isPaused() && !waitingForCancel)
      Thread::sleep(100);
    return number;
  }
  friend ostream&
  operator<<(ostream& os, const Entrance& e) {
    return os << "Entrance " << e.id << ": " << e.number;
  }
};

int main() {
  srand(time(0)); // Seed the random number generator
  cout << "Press <ENTER> to quit" << endl;
  CountedPtr<Count> count(new Count);
  vector<Entrance*> v;
  CountedPtr<Display> display(new Display);
  const int SZ = 5;
  try {
    ThreadedExecutor executor;
    for(int i = 0; i < SZ; i++) {
      Entrance* task = new Entrance(count, display, i);
      executor.execute(task);
      // Save the pointer to the task:
      v.push_back(task);
    }
    cin.get(); // Wait for user to press <Enter>
    count->pause(); // Causes tasks to stop counting
    int sum = 0;
    vector<Entrance*>::iterator it = v.begin();
    while(it != v.end()) {
      sum += (*it)->getValue();
      ++it;
    }
    ostringstream os;
    os << "Total: " << count->value() << endl
       << "Sum of Entrances: " << sum << endl;
    display->output(os);
    count->cancel(); // Causes threads to quit
  } catch(Synchronization_Exception& e) {
    cerr << e.what() << endl;
  }
} ///:~

Listado 10.25. C11/OrnamentalGarden.cpp


Count es la clase que conserva el contador principal de los visitantes del jardín. El objeto único Count definido en main() como contador FIXME is held como un CountedPtr en Entrance y, así, se comparte entre todos los objetos Entrance. En este ejemplo, se utiliza un FastMutex llamado lock en vez de un Mutex ordinario ya que un FastMutex usa el mutex nativo del sistema operativo y, por ello, aportará resultados más interesantes.

Se utiliza un Guard con bloqueo en increment() para sincronizar el acceso a count. Esta función usa rand() para realizar una carga de trabajo alta (mediante yield()) FIXME la mitad del tiempo, ....

La clase Entrance también mantiene una variable local numbre con el número de visitantes que han pasado a través de una entrada concreta. Esto proporciona un chequeo doble contra el objeto count para asegurar que el verdadero número de visitantes es el que se está almacenando. Entrance::run() simplemente incrementa number y el objeto count y se duerme durante 100 milisegundos.

En main, se carga un vector<Entrance*> con cada Entrance que se crean. Después de que el usuario pulse Enter, el vector se utiliza para iterar sobre el valor de cada Entrance y calcular el total.

FIXMEThis program goes to quite a bit of extra trouble to shut everything down in a stable fashion.

Toda la comunicación entre los objetos Entrance ocurre a través de un único objeto Count. Cuando el usuario pulsa Enter, main() manda el mensaje pause() a count. Como cada Entrance::run() está vigilando a que el objeto count esté pausado, esto hace que cada Entrance se mueva al estado waitingForCancel, donde no se cuenta más, pero aún sigue vivo. Esto es esencial porque main() debe poder seguir iterando de forma segura sobre los objetos del vector<Entrace*>. Note que debido a que existe una FIXMEpequeña posibilidad que la iteración pueda ocurrir antes de que un Entrance haya terminado de contar y haya ido al estado de waitingForCancel, la función getValue() itera a lo largo de las llamadas a sleep() hasta que el objeto vaya al estado de waitingForCancel. (Esta es una forma, que se conoce como espera activa, y es indeseable. Verá un enfoque más apropiado utilizando wait(), más adelante en el capítulo). Una vez que main() completa una iteración a lo largo del vector<Entrance*>, se manda el mensaje de cancel() al objeto count, y de nuevo todos los objetos Entrance esperan a este cambio de estado. En ese instante, imprimen un mensaje de finalización y salen de run(), por lo que el mecanismo de hilado destruye cada tarea.

Tal y como este programa se ejecuta, verá que la cuenta total y la de cada una de las entradas se muestran a la vez que la gente pasa a través de un FIXMEturnstile. Si comenta el objeto Guard en Count::increment(), se dará cuenta que el número total de personas no es el que espera que sea. El número de personas contadas por cada FIXMEturnstile será diferente del valor de count. Tan pronto como haya un Mutex para sincronizar el acceso al Counter, las cosas funcionarán correctamente. Tenga en cuenta que Count::increment() exagera la situación potencial de fallo que supone utilizar temp y yield(). En problemas reales de hilado, la probabilidad de fallo puede ser estadísticamente menor, por lo que puede caer fácilmente en la trampa de creer que las cosas funcionan correctamente. Tal y como muestra el problema anterior, existen FIXMElikely problemas ocultos que no le han ocurrido, por lo que debe ser excepcionalmente diligente cuando revise código concurrente.

Operaciones atómicas

Note que Count::value() devuelve el valor de count utilizando un objeto Guard para la sincronización. Esto ofrece un aspecto interesante porque este código probablemente funcionará bien con la mayoría de los compiladores y sistemas sin sincronización. El motivo es que, en general, una operación simple como devolver un int será una operación atómica, que quiere decir que probablemente se llevará a cabo con una única instrucción de microprocesador y no será interrumpida. (El mecanismo de multihilado no puede parar un hilo en mitad de una instrucción de microprocesador.) Esto es, las operaciones atómicas no son interrumpibles por el mecanismo de hilado y, así, no necesitan ser protegidas.[152] De hecho, si elimináramos la asignación de count en temp y quitáramos yield(), y en su lugar simplemente incrementáramos count directamente, probablemente no necesitaríamos un lock ya que la operación de incremento es, normalmente, atómica. [153]

El problema es que el estándar de C++ no garantiza la atomicidad para ninguna de esas operaciones. Sin embargo, pese a que operaciones como devolver un int e incrementar un int son atómicas en la mayoría de las máquinas no hay garantías. Y puesto que no hay garantía, debe asumir lo peor. En algunas ocasiones podría investigar el funcionamiento de la atomicidad para una máquina en particular (normalmente mirando el lenguaje ensamblador) y escribir código basado en esas asunciones. Esto es siempre peligroso y FIXMEill-advised. Es muy fácil que esta información se pierda o esté oculta, y la siguiente persona que venga podría asumir que el código puede ser portado a otra máquina y, por ello, volverse loco siguiendo la pista al FIXMEoccsional glitch provocado por la colisión de hilos.

Por ello, aunque quitar el guarda en Count::value() parezca que funciona no es FIXMEairtight y, así, en algunas máquinas puede ver un comportamiento aberrante.