Bienvenidos sean a este post, hoy veremos como implementar una tecnica para los threads.
Para poder implementar un stack de tipo thread-safe usando locks debemos utilizar a adaptadores de contenedores, de los cuales hablamos en este post. Sabemos que el stack tiene dos operaciones basicas, push y pop, las cuales se encargan de modificar el estado del contenedor. Y tambien sabemos que el stack no es un contenedor en si mismo; es un adaptador que envuelve un contenedor y provee una interfaz adaptada para acceder. Mas alla de los metodos comentados y el de construccion y destruccion, stack tambien provee los siguientes:
- top, accede al elemento superior de stack
- empty, devuelve si el stack esta vacio o no
- size, devuelve el tamaño actual del stack
- push, ingresa un nuevo elemento al inicio del stack
- emplace, construye un nuevo elemento al inicio del stack
- pop, quita el elemento al inicio del stack
- swap, intercambia el contenido con otro stack
Si bien con todos estos metodos podemos crear un stack completo pero por el momento nos centraremos en un stack thread-safe, y por ello usaremos principalmente a push y pop. Estos metodos pueden corromper la estructura de datos si varios threads interfieren uno con el otro. Veamos la siguiente clase que representa a un stack thread-safe:
template <typename T>
class stack_seguro
{
public:
stack_seguro();
stack_seguro(const stack_seguro& otro);
void push(T valor);
void pop();
T& top();
bool empty() const;
private:
std::stack<T> wrappee_;
mutable std::mutex mutex_;
};
Tenemos al constructor predeterminado y otro que recibe otro objeto para hacer una copia, asi como otros prototipos para los distintos metodos y acciones de la clase. En la parte privada tenemos una curiosidad, es que declaramos a mutex como mutable porque lo bloqueamos en el metodo empty que es constante. Si bien, una mejor decision de diseño es quitar la opcion de constante al metodo empty, sin embargo ya deberian saber que si usamos a mutable para cualquier miembro de dato nos sugiere que hemos tomado malas decisiones de diseño. Pasemos a ver las implementaciones de los prototipos, comenzando con el constructor de copia:
stack_seguro::stack_seguro(const safe_stack& otro)
{
std::lock_guard<std::mutex> lock(otro.mutex_);
wrappee_ = otro.wrappee_;
}
Primero bloquearemos al mutex pero del objeto que vamos a copiar. Esto puede parecer injusto pero en realidad es para asegurarnos que el objeto recibido no sufra ningun cambio antes de la copia. Y a la propiedad wrapee le asignamos al de la copia. Pasemos a definir al metodo push:
void stack_seguro::push(T valor)
{
std::lock_guard<std::mutex> lock(mutex_);
wrappee_.push(std::move(valor));
}
Este metodo es bastante simple, donde primero bloquearemos al mutex para luego mover la informacion en el stack. Pero observen que al momento de usar push le pasamos el valor mediante la funcion move. Esta funcion es usada para indicar que un objeto generico podria ser «movido desde», esto puede garantizarnos una eficiente transferencia de recursos desde un objeto generico a otro. Esto que comentamos recien se incorpora a todas las funciones, la sincronizacion mediante thread. Donde primero bloqueamos al mutex, hacemos el trabajo y luego desbloquear al mutex. Como estuvimos comentando hasta ahora, esta accion nos asegura que solo un thread acceda a la informacion por vez y nos protega la informacion de las condiciones de carrera. Pasemos a analizar la declaracion del metodo pop:
std::shared_ptr<T> pop()
{
std::lock_guard<std::mutex> lock(mutex_);
if (wrappee_.empty()) {
throw std::exception("El stack esta vacio");
}
std::shared_ptr<T> elemento_top{std::make_shared<T>(std::move(wrappee_.top()))};
wrappee_.pop();
return elemento_top;
}
Nuevamente, bloqueamos al mutex pero hacemos un chequeo si el stack esta vacio. En caso de ser verdadero, lanza una excepcion para indicar que el mismo se encuentra vacio y no realizar ninguna accion. En caso contrario, creamos un objeto que almacenara el elemento top de nuestro stack y tambien lo moveremos. Para luego eliminarlo mediante pop y finalmente devolver este valor pero por que hacemos esto? Esto lo hacemos porque la mayoria de las veces no queremos a alguien que pueda acceder al top del stack (con una referencia) y luego pueda eliminar ese dato desde otro thread. Observemos que la declaracion de stack_seguro deberia cambiar de acuerdo a las modificaciones de la funcion pop, y ya no sea necesaria la funcion top.
En resumen, hoy hemos visto como implementar un stack libre de threads, como se debe hacer, las funciones que podemos usar, asi como las que podemos sobrecargar para poder implementarla. Espero les haya sido de utilidad sigueme en tumblr, Twitter o Facebook para recibir una notificacion cada vez que subo un nuevo post en este blog, nos vemos en el proximo post.


Donación
Es para mantenimento del sitio, gracias!
$1.50
