Bienvenidos sean a este post, hoy veremos algunas operaciones disponibles.
En este post hablamos por primera vez sobre las brecha entre lineas y pueden generar la condicion de carrera. En el post anterior hablamos sobre los tipos atomicos y mencionamos que su objetivo principal es eliminar estas brechas. Pero otra posibilidad es proveer operaciones que se ocupen de ello mediante varias funciones combinadas envueltas en una sola funcion. Veamos algunas operaciones en tipos atomicos:
- load
- store
- exchange
- compare_exchange_weak
- compare_exchange_strong
- wait
- notify_one
- notify_all
La operacion load automaticamente carga y devuelve un valor a una variable atomica. La operacion store reemplaza atomicamente el valor de una variable atomica con un argumento no-atomico. Tanto load y store son operaciones similares a lectura y asignacion de variables no-atomicas. Cuando accedemos a un valor de un objeto, ejecutamos una instruccion de lectura. Veamos el siguiente ejemplo:
double d{4.2};
std::cout << d;
Estas son operaciones no-atomicas, la primera es para asignar un valor a la variable y la segunda es de lectura del contenido de la variable anterior. Veamos lo mismo que antes pero en su version atomica:
atomic_int m;
m.store(42);
std::cout << m.load();
Primero declaramos la variable, luego mediante store le asignamos un valor a la variable y con load haremos la lectura del valor. Con esto podemos ver la diferencia entre atomicas y no-atomicas y porque los tipos atomicos deben ser realizadas a traves de operaciones atomicas. Veamos como es la definicion de las funcion load, store y exchange:
T load(std::memory_order order = std::memory_order_seq_cst) const noexcept;
void store(T value, std::memory_order order = std::memory_order_seq_cst) noexcept;
T exchange(T value, std::memory_order order = std::memory_order_seq_cst) noexcept;
En todos los casos tenemos una variable llamada order de tipo memory_order pero sobre esto hablaremos en un momento. En store tenemos otra variable para recibir el valor a asignar. Si observan la ultima funcion es una comprension de los dos anteriores, ya que reemplaza atomicamente el valor con el recibido como argumento y atomicamente obtiene el valor previo. Veamos la definicion de las siguientes dos funciones:
bool compare_exchange_weak(T& expected_value, T target_value,
std::memory_order order =
std::memory_order_seq_cst) noexcept;
bool compare_exchange_strong(T& expected_value, T target_value,
std::memory_order order =
std::memory_order_seq_cst) noexcept;
Estas funciones son muy similares porque comparan dos valores entre si. Comparando el primer argumento con la variable atomica. Si son iguales, reemplaza la variable con el segundo argumento. De lo contrario, cargan atomicamente los valores dentro del primer argumento (por eso son pasados como referencias). La diferencia entre ellos es que compare_exchange_weak tiene permitido fallar falsamente (tambien denominado como falla espuria), incluso cuando expected_value es igual al valor subyacente, la funcion la trata como no igual. Esto es asi porque en algunas plataformas nos dirige a un incremento de la performance.
Las funciones wait, notify_one, notify_all fueron agregadas en C++20. La funcion wait bloquea el thread hasta que el valor del objeto atomico se modifica. Este toma un argumento para compararlo con el valor del objeto atomico, donde si los valores son iguales bloquea al thread. Para poder desbloquear al thread manualmente tenemos a notify_one o notify_all. La diferencia entre ellos es que notify_one desbloquea solo una operacion de bloqueo, en cambio la otra desbloquea a todas.
Vamos a comentar sobre el tipo memory_order que mencionamos en las primeras operaciones. Esta define el orden de acceso a la memoria alrededor de las operaciones atomicas. Cuando multiples threads escriben y leen a variables simultaneamente, un thread puede leer los cambios en un orden diferente desde el orden en el cual otro thread los almaceno. El orden predeterminado para las operaciones atomicas es el orden consistente secuencialmente, de esto se encarga memory_order_seq_cst. Veamos algunas operaciones mas que disponemos:
- fetch_add
- fetch_sub,
- fetch_or
- fetch_and
- fetch_xor
fetch_add se encarga de aplicar la operacion de adicion y reemplaza atomicamente al valor del objeto con el resultado de la operacion. fetch_sub hace lo mismo pero con la operacion de substraccion, siendo para la operacion post-incremento y post-decremento respectivamente. Los otras operaciones hacen lo mismo pero aplicando el resultado de la operacion OR, AND y XOR respectivamente entre el valor recibido y el contenido por el objeto.
Para finalizar, tambien existe un tipo atomico denominado como atomic_flag, este es un valor de tipo booleano y tiene garantizado que es lock-free, asi como tambien no posee operaciones de load y store. Pero si posee estas dos operaciones:
- clear
- test_and_set
Como dijimos, este es un valor booleano y podemos considerarlo como un bit atomico. Por lo tanto, la operacion clear es para vaciarla o limpiarla. La operacion test_and_set cambia el valor a true y devuelve su valor previo.
En resumen, hoy hemos visto las operaciones en tipos atomicos, cuales son, para que sirven, como se comportan, asi como algunos comentarios extras sobre las mismas. Espero les haya resultado 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
