Existen muchos métodos de programación, lenguajes de desarrollo con distintas capas de complejidad de aprendizaje y tendencias nuevas en la industria de la programación computacional que someten a los diseñadores de software a cruentas batallas con cada vez mayores enemigos virtuales, uno de ellos los defectos de concurrencia. ¿Cómo afrontarlos?
(ElectronicosOnline.com Magazine / Oswaldo Barajas)
En la actualidad las capas de complejidad y abstracción de los distintos y variados lenguajes de programación resultan tan inverosÃmiles que los mismos diseñadores de software deben flexibilizar sus conocimientos para entender los distintos patrones de solución que en particular requiere cada lenguaje para resolver los conflictos y en esto, someterse a una cruenta batalla con enemigos virtuales que aparecen en la etapa de desarrollo tales como los defectos de concurrencia.

Muchos de los conflictos de desarrollo de software nacen a partir de que el hardware ha sido acondicionado con componentes más potentes como lo son los procesadores con arquitectura multi-core, o bien aquellos chips de procesamiento de datos que de manera interna poseen virtualización fÃsica de su núcleo principal para dividirse en otros pequeños núcleos con funciones semi-independientes y especializados para realizar tareas individuales.
Es aquà donde los desarrolladores de software se enfrentan con un gran obstáculo, pues en tanto el hardware adquiere rasgos multi-núcleos la manera de crear aplicaciones por parte de los ingenieros desarrolladores debe también ser modificada.
Lo que regularmente está acostumbrado a observarse en los equipos de desarrollo de software, es que éstos se centran en las aplicaciones multi-hilo para trabajar con el hardware multi-núcleo, pues de esta manera se cubren las necesidades del proyecto, pero al mismo tiempo se amplÃa el rango de vulnerabilidad para que aparezcan los errores de concurrencia de desarrollo de software, que son prácticamente conflictos de ejecución en las lÃneas del código que se producen cuando se utilizan aplicaciones multi-hilo.

Algunos de estos defectos o errores de concurrencia son los denominados “race conditions†ó condiciones de carrera, definidos también como múltiples procesos que están en condición de carrera cuando el resultado de los mismos depende del orden en que se ejecute.
En caso de que los procesos que están en condición de carrera no son correctamente sincronizados, el programador puede experimentar en su proyecto errores de corrupción de datos, lo que puede ser aprovechado por explotadores locales que reducen la protección de los sistemas.
Cabe mencionar que la condición de carrera o el estado de carrera también se presentan en los circuitos electrónicos cuando la salida de un sistema o sub-sistema depende del orden en que se hayan activado o desactivado los componentes.
De manera resumida, la condición de carrera aparece cuando varios procesos acceden al mismo tiempo a un recurso compartido, por ejemplo una variable a la que se le cambia su estado y se obtiene de ésta un valor no esperado como resultado y también aprovechando que se trabaja sobre un entorno de aplicaciones multi-hilo para proyectos de hardware multi-core.
La complejidad anteriormente mencionada es tan grave que si el mismo programador o su método de depuración o análisis de código, pasa por alto el error de concurrencia una vez que el proyecto ha terminado o se encuentra en etapas finales, las consecuencias son devastadoras en términos económicos, de tiempo y esfuerzos.

El modelo tradicional de resolución para los errores de concurrencia como las condiciones de carrera inician detectando la fila que produjo el error y una estrategia para controlarlo, pero será más seguro aplicar el método por análisis estático.
El análisis de código con la técnica de análisis estático puede suministrar la facilidad de detección y solución de defectos de concurrencia en un nuevo código, y también permite a los usuarios reducir los costos de inversión para aquel software contaminado en su código con defectos y diseñado para correrse con procesadores multi-hilos.
A continuación contamos con algunos ejemplos de cómo varÃa el nivel de complejidad entre los sistemas multi-hilo y aquellos con un solo hilo. (Esta información ha sido extraÃda como base de referencia editorial de la aportación del especialista desarrollador de software y hardware Mars Rover, ingeniero durante varios años para el grupo de IngenierÃa de la NASA.)

La selección anterior fue puesta por el especialista Rovers a fin de que crear un simple programa que se sirva de algunas entradas del usuario e imprima un mensaje basado en el valor de la entrada del usuario. Cuando se ejecuta en un sistema de un solo hilo, resulta sencillo aplicar un esquema de probación  para todas las posibles entradas obligará al ingeniero desarrollador asegurarse  de poner un valor de retroalimentación al programa menor a 100, también un valor mayor o igual a 100, aclarando que lo anterior asumirá que nada interesante sucederá en el “input_from_user†admitiendo una mala suposición en el desarrollo del software.
“Ahora vamos a incrementar la participación e imaginar que el usuario final quiso este programa con el ingreso de dos valores en lugar de uno con la misma lógica después de la entrada. Este cambio aumentarÃa la complejidad de la aplicación de un solo hilo y en tal caso el desarrollador del software necesitarÃa explorar cuatro posibles opciones para probar el programaâ€, indica el documento de Mars Rovers.
En este sentido Rovers añadió que estos cambios podrÃan requerir que el código expuesto en la figura 1fuera ejecutado simultáneamente por dos diferentes hilos para tomarse en dos entradas. Y suponiendo que cada comunicación fuera hecha por tres instrucciones de la máquina, el número de posibles combinaciones o intercalaciones de las instrucciones en los dos hilos permitirÃan aproximadamente 194, 480 posibilidades diferentes de ejecución.
Ejemplo de condición de carrera
Una de las partes más complicadas en un proyecto de desarrollo de software en donde se ven implicados sistemas multi-hilos con hardware multi-core, es que cuando se somete el código a una depuración en el que se desea identificar errores de concurrencia automáticamente se convierte en todo un reto.
Al decir del experto en la materia Mars Rovers, el análisis del software es complejo en un entorno multi-hilos debido a que los problemas son difÃciles de reproducir en estas aplicaciones una vez que se ejecutan. Asimismo el sistema operativo y el programador de hilos deciden en conjunto cuándo serán ejecutados cada hilo.
Otro de los factores que dificultan la depuración del código con errores de concurrencia es que el rastreo o el seguimiento de lÃnea que causa el problema como por ejemplos un comportamiento inesperado en el sistema es casi imposible mediante una depuración manual como lo muestra otro de los ejemplos que proporcionó el especialista:

Ejemplo de una condición de carrera
Rovers compartió este ejemplo en código Java en donde la variable “lock†protege la variable “count†en el tipo “GuardedByViolationExampleâ€.  En el método “decrement()†el acceso a “count†no está sincronizado lo cual al momento de ejecutarse en un ambiente multi-hilos da lugar a un error de concurrencia mediante una condición de carrera.
Por consiguiente el valor de “count†será inesperado al momento de que más de un hilo se ejecuta para actualizar el valor de “countâ€. Rovers puntualizó que sin conocer la ubicación del error el acceso a “count†en “decrement()†carece de sincronización desatarÃa un ambiente más complicado para dar seguimiento a la fuente del error de dicho comportamiento atÃpico del sistema ya que el único valor observable es el inesperado “countâ€.
Otro resultado de esta situación son los llamados “deadly deadlocks†o aquellas reacciones del sistema en donde un programa multi-hilos no puede realizar ningún progreso, la definición podrÃa ser considerada algo asà como “puntos muertos fatales†del sistema y resulta algo no menos complejo para depurar tal y como se presenta en la siguiente figura:

Ejemplo de un punto muerto fatal
Cuando dos hilos definidos llaman la función “deadlock_partA()†y “deadlock_partB()†el primero en presentarse podrÃa ser “(a-)lock)†mientras que el segundo serÃa “b->lockâ€. En este momento resulta imposible para cada hilo adquirir el lock o llave que necesita para continuar con la ejecución o la liberación de la llave que guarda. Lo peor –según indica Rovers- es que el deadlock existente puede involucrar la adquisición de múltiples llaves en todos los hilos.
Análisis Estático, la solución
Como bien planteó un destacado ingeniero programador de nombre Ivar Jacobson: “El desarrollo de software nunca ha sido tan complejo como lo es ahora. Los desarrolladores de software trabajan intensivamente con el conocimiento. No sólo deben comprender nuevas tendencias y tecnologÃas, sino que necesitan saber cómo aplicarlas de forma rápida y productiva (…)â€, partiendo de este argumento definimos que uno de los recursos propuestos por el especialista  Mars Rovers, es el análisis estático del código que no es otra cosa más que un proceso de evaluación del software sin ejecutarlo.
Es también una técnica que se aplica sobre el código fuente sin necesidades de transformaciones previas ni cambios de ningún tipo. El objetivo del mismo es que en base a ese código fuente se logre obtener información que permita mejorar la base de código dejando la estructura o semántica original.
El analizador estático de código recibe de esta manera el código fuente del programa evaluado y lo procesará intentando averiguar qué es lo que deseamos que realice y de manera práctica ofrezca sugerencias prácticas al ingeniero desarrollador sobre cómo optimizar o mejorar el código.
En el ejemplo de la figura número 2 propuesto por Rovers es obvio que el acceso a la variable “count†requiere de una sincronización. Una vez accedido a “count†en un bloque sincronizado, lo intercalado llama a incrementar la misma instancia del objeto que son eliminados.
“El análisis estático ayuda a los programadores a evitar condiciones de carrera infiriendo y reforzando “lo protegido por†las relaciones en campos compartidos del programaâ€, menciona el documento de Mars Rovers. “Un campo (f) es protegido por lock (I) si accede a “f†debe ser protegido por sujeción de lock I y esto definirá esencialmente la disciplina para cada clase de programa a evaluar.â€
Lo mismo ocurre para detectar los puntos muertos fatales del código y otras condiciones de error de concurrencia cuando los desarrolladores de software trabajan con aplicaciones multi-hilo.