jueves, 4 de septiembre de 2014

Receta Multithreading en C# No. 2-1: Ejecución de Operaciones Atómicas Básicas

Índice

0. Introducción
1. Problema
2. Solución
3. Discusión de la Solución
4. Práctica: Código C#
5. Conclusiones
6. Glosario
7. Literatura & Enlaces

0. Introducción

Esta receta comprende la primera de la serie de recetas multithreading dedicadas al estudio de la sincronización de threads. Veremos distintos métodos de sincronización como: exclusión mutua, uso de semáforos, eventos, entre otros. Estos métodos nos brindará las herramientas para la creación de aplicaciones capaces de aprovechar las capacidades multithreading tanto del sistema operativo como del hardware adyacente; y por supuesto controlar el acceso a los recursos compartidos por una aplicación o procesos para evitar a toda costa la generación de resultados inconsistentes o corruptos. En esta primera parte aprenderemos acerca la ejecución de operaciones atómicas básicas para la prevención de la condición de carrera (o en inglés, race condition).

1. Problema

Tenemos un problema en el acceso y modificación de valores de variables por diferentes threads: dos o más threads intentan cambiar el valor de una variable al mismo tiempo, por ejemplo, en la operación de incremento. El resultado no coincide con el esperado al final de la ejecución: es inconsistente.

2. Solución

En sistemas software este tipo de inconsistencias en los resultados que se generan por el acceso simultáneo al valor de una variable y su respectivo valor por parte de dos o más threads se conoce como condición de carrera (o race condiction). Para atacar este problema, C# cuenta con la construcción (clase) Interlocked.

3. Discusión de la Solución

La clase Interlocked [3] es una clase static definida en el nombre de espacios System.ThreadingSu tarea es proveer operaciones atómicas (e.g., aritméticas, e intercambio de valores) sobre valores de datos compartidos asegurando su consistencia a través de la seguridad en el acceso concurrente por múltiples threads. Cuenta con los siguientes miembros método:
  • Add [4]: Método sobrecargado para operación atómica de suma. (2 versiones sobrecargadas.)
  • CompareExchange: Compara dos valores de datos, y alterna su valor en caso de ser iguales. (Hasta 7 versiones sobrecargadas.)
  • Decrement [7]: Decrementa atómicamente en una unidad el valor de un dato. (2 versiones sobrecargadas.)
  • Exchange [8]: Alterna (asignación) el valor de un valor de dato. (7 versiones sobrecargadas).
  • Increment [6]: Incrementa en una unidad el valor de un dato. (2 versiones sobrecargadas).
  • MemoryBarrier: Sincroniza el acceso a memoria.
  • Read: Lee el valor almacenado en valor de dato.
[Nota: En la receta tipo C# Sincronizar el Acceso a Valores de Datos Compartidos hay un amplio cubrimiento de los principales métodos de la clase Interlocked, la cual recomiendo su lectura y estudio de los ejemplos ahí presentados para comprender su funcionamiento y utilidad.]

Por otro lado, comencemos por hablar más detenidamente acerca del concepto de condición de carrera.

De acuerdo con [2] la condición de carrera consiste la dependencia de un proceso sobre la secuencia o sincronización cronológica con la que debe ejecutar un grupo de threads para la generación de resultados consistentes sobre datos de aplicación o de usuario. Para evitar la corrupción o inconsistencia, estos valores de datos deben ser accedidos en instrucciones de una sección o región crítica de acceso exclusivo, es decir a instrucciones de acceso secuencial (los threads se ejecutan en serie sobre esa región crítica); a esto se le conoce como sincronización de threads.

Además, como advierten en [2], una condición de carrera de threads comprende una tarea difícil de reproducir y depurar debido a la naturaleza no-determinística en los tiempos de ejecución de los threads. Otra advertencia importante en [2] es:
«...avoid race conditions by careful software design rather than attempting to fix [problems later]»
Un ejemplo clásico en donde podemos visualizar la condición de carrera consiste en el incremento o decremento del valor de una variable entera. Veamos:

En primer lugar empecemos por conocer la situación ideal y esperada sobre el incremento unitario de una variable entera:
Incremento múltiple threads esperado
Figura 1. Incremento múltiple threads esperado.
Los threads Thread No. 1, y Thread No. 2 se hayan correctamente sincronizados, es decir, la secuencia en la que ocurren los eventos de incremento de la variable entera es correcta y genera el resultado esperado, en este caso, 2.

Cuando múltiples threads carecen de sincronización, obtenemos:
Incremento múltiple threads inesperado
Figura 2. Incremento múltiple threads inesperado.
Los dos threads Thread No. 1, y Thread No. 2 acceden simultáneamente a la ubicación de memoria e intentan del mismo modo modificar el valor entero de la variable, pero al final sólo se escribirá en memoria, a través de la operación Grabar valor, un único incremento: 1, debido a que no ocurren secuencialmente. El resultado no es el esperado como en el caso anterior (ver Figura 2), para el caso, 2.

En el ejemplo que veremos en la siguiente sección demostraremos cómo atacar este problema con el uso de la clase static Interlocked.

4. Práctica: Código C#

En esta sección vamos a presentar dos clases: la primera provee el mecanismo no controlado (sin sincronización) de incremento y decremento del valor de una variable entera; la segunda hace uso de los métodos static de Interlocked para sincronizar las operaciones de incremento y decremento de una variable entera.

Resaltemos el uso de los métodos Incrementar y Decrementar de las clases ContadorEstandar y ContadorSincronizado:
  • Clase ContadorEstandar (líneas 81-102):
    • Método Incrementar (líneas 93-96): Incrementa la variable contador utilizando el operador de incremento unitario compuesto: ++. Esta forma no sincroniza el acceso por múltiples threads, por lo tanto puede generar resultados inconsistentes.
    • Método Decrementar (líneas 98-101): Decrementa la variable contador utilizando el operador de decremento unitario compuesto: --. Esta forma no sincroniza el acceso por múltiples threads, por lo tanto puede generar resultados inconsistentes.
  • Clase ContadorSincronizado (líneas 106-127):
    • Método Incrementar (líneas 118-121): Incrementa la variable contador de forma sincrónica a través del método Interlocked.Increment. Cualquier intento simultáneo es controlado para evitar la generación de resultados inconsistentes, tal cual como se esquematizó en la Figura 2 de la sección 3.
    • Método Decrementar (líneas 123-126): Decrementa la variable contador de forma sincrónica a través del método Interlocked.Decrement.
Desde Main (líneas 8-58) se invoca al método static para realizar 100 mil invocaciones de los métodos mencionados por cada instancia de las clases mencionadas.

Compilación:

  1. csc /target:exe Contadores.cs

Ejecución assembly:

  1. .\Contadores.exe


> Prueba de ejecución (local):
Ejecución assembly Contadores.exe
Figura 3. Ejecución assembly Contadores.exe.

5. Conclusiones

Asegurar que los resultados de operaciones simultáneas llevadas a cabos por múltiples threads es una tarea que debe tomarse muy en serio el programador de sistemas informáticos. La generación de resultados inconsistentes puede llevar a tomar malas decisiones por parte del usuario de la aplicación. De ahí que debamos hacer hincapié en la exploración y aplicación de los elementos de programa que provee la biblioteca base de clases de .NET Framework para la sincronización de acceso a regiones críticas de código (como el ejemplo de la sección 4: incrementar y decrementar valores enteros). En la próxima receta multithreading utilizaremos la clase Mutex para sincronizar threads.

6. Glosario

  • Condición de carrera
  • Depurar
  • Race condition
  • Sincronización
  • Thread

7. Literatura & Enlaces

[1]: Multithreading in C# 5.0 Cookbook by Eugene Agafonov. Copyright 2013 Eugene Agafonov, 978-1-84969-764-4.
[2]: Race condition - Wikipedia, the free encyclopedia - https://en.wikipedia.org/wiki/Race_condition
[3]: Interlocked Class (System.Threading) - http://msdn.microsoft.com/en-us/library/system.threading.interlocked%28v=vs.110%29.aspx
[4]: Interlocked.Add Method (System.Threading) - http://msdn.microsoft.com/en-us/library/system.threading.interlocked.add(v=vs.110).aspx
[5]: Interlocked.Add Method (Int32, Int32) (System.Threading) - http://msdn.microsoft.com/en-us/library/33821kfh(v=vs.110).aspx
[6]: Interlocked.Increment Method (System.Threading) - http://msdn.microsoft.com/en-us/library/system.threading.interlocked.increment(v=vs.110).aspx
[7]: Interlocked.Decrement Method (Int32) (System.Threading) - http://msdn.microsoft.com/en-us/library/1z4b2e5y(v=vs.110).aspx
[8]: Interlocked.Exchange Method (System.Threading) - http://msdn.microsoft.com/en-us/library/system.threading.interlocked.exchange(v=vs.110).aspx
[9]: Receta C# No. 4-11: Sincronizar el Acceso a Valores de Datos Compartidos | OrtizOL - Experiencias Construcción Software (xCSw) - http://ortizol.blogspot.com/2014/08/receta-csharp-no-4-11-sincronizar-el-acceso-a-valores-de-datos-compartidos.html


J

No hay comentarios:

Publicar un comentario

Envíe sus comentarios, dudas, sugerencias, críticas. Gracias.