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.
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.