Está en la página 1de 23

/40+

3.M54RFC |

A3. Reporte de
Programación Investigación
Concurrente
Condiciones de Carrera · Interbloqueo ·
Automátas Finitos determinísticos

Profesor Mtro. Alan Javier Massoco Dossetti

Josafat Emanuel Díaz Martínez


Equipo
Progra_concu 4 Daniel Espinosa Jiménez
Manuel de Jesús Mayorga González

2023-C1 2023-02-06
Contenido
1 Introducción .......................................................................................................................................... 2
2 Investigación ......................................................................................................................................... 4
2.1 Condiciones de carrera ................................................................................................................. 4
2.1.1 Teoría, causas........................................................................................................................ 4
2.1.1.1 Condiciones de Bernstein y secciones paralelizables ....................................................... 4
2.1.1.2 Secciones críticas .............................................................................................................. 5
2.1.1.3 Race conditions por tipo de recursos compartidos .......................................................... 5
2.1.2 Código de ejemplo explicado ................................................................................................ 6
2.2 Interbloqueo ................................................................................................................................. 8
2.2.1 Teoría, causas........................................................................................................................ 8
2.2.1.1 El problema de la exclusión mutua y los semáforos ......................................................... 8
2.2.1.2 El problema del interbloqueo ........................................................................................... 9
2.2.2 Código de ejemplo explicado .............................................................................................. 11
2.3 Autómatas Finitos Deterministas (DFAs) .................................................................................... 14
2.3.1 Ejercicio 1 ............................................................................................................................ 16
2.3.2 Ejercicio 2 ............................................................................................................................ 18
3 Conclusiones ....................................................................................................................................... 19
4 Referencias APA 7 ............................................................................................................................... 20
Anexo 1. Diagrama de máquina de estado ................................................................................................. 22

1
1 Introducción
[incluir OpenMP, MPI, sección critica y race cond, interbloqueo, soluciones, AFDs]

Partiendo de los conceptos usados en programación concurrente cubiertos en las tareas anteriores,
surge ahora la necesidad de analizar sistemas concurrentes, de tal manera que se puedan entender y
expresar los problemas comunes que surgen, así como expresar y modelar soluciones para ellos.

¿Qué clase de problemas? Los enumeramos en el mapa mental de la Actividad 1, citamos aquí el
fragmento correspondiente:

Fig. 1. Problemas debido a la concurrencia. Tomado de nuestra actividad actividad A1. Mapa mental
(2023).

En este trabajo veremos de qué trata race condition, así como el término Interbloqueo, que comprende
el candado muerto, y podríamos agregar a la lista algunos problemas más, como el uso excesivo de
variables globales (multithreading), que en sí mismo no va a causar una salida incorrecta del programa,
pero sincronizar hilos implica un costo de uso de CPU, y estar todo el tiempo sincronizando las variables
globales es muy probable que el OS termine corriendo el proceso en un solo core, equivaldrá a ejecutar
un algoritmo secuencial, eliminando toda ventaja de la programación concurrente, por lo que hay que
reducir al máximo el uso de variables globales, entre otras técnicas que mejoran ya sea el algoritmo y/o
la implementación del mismo.

Así, veremos que es fundamental saber, comprender lo que estamos haciendo y lo que queremos hacer,
y que el énfasis está en los algoritmos, seguido del uso apropiado del lenguaje de programación.

Modelar un algoritmo, cuyo objetivo es resolver el problema computacional planteado, requerirá de


varias técnicas, dependiendo tanto la naturaleza del problema (modelos matemáticos y otros tipos de
modelos de sistemas), como de la complejidad y características de la solución planteada. Ello implica
analizar los pasos del algoritmo para identificar secciones que pueden ser paralelizadas si cumplen las
Condiciones de Bernstein, y podemos representarlo tanto usando pseudocódigo, y grafos muy sencillos
de precedencia, como también documentamos en nuestro mapa mental de la Actividad 1, de donde
tomamos el siguiente extracto:

2
Fig. 2. Teoría y herramientas básicas para el diseño de algoritmos concurrentes o paralelos. Tomado de
nuestra actividad A1. Mapa mental (2023).

Claro, esas son herramientas muy básicas, mientras que, como vimos en el cuadro comparativo de la
Actividad 2, un proceso multihilo o un programa multiproceso (aquel que lanza procesos hijos) son ya de
por sí complejos, pero cuando se trata de sistemas distribuidos, esa complejidad aumenta demasiado.

Así que hoy en día tenemos varias técnicas de modelado de sistemas, destacando la especificación de
UML (Unified Modeling Language), el cual nos ofrece varios tipos de diagramas que se pueden
complementar para describir el sistema, dependiendo de la expresividad que necesitemos en nuestros
análisis, podemos usar diagramas de estructura estática como los diagramas de clase, de objetos, de
componentes y compuestos (diagramas de colaboración), así como diagramas de deployment. Esos son
útiles, pero para analizar la ejecución del algoritmo concurrente o paralelo, se requieren diagramas de
comportamiento (behavior diagrams). En ese rubro los más útiles son los diagramas de actividad
(diagramas de flujo), diagramas de interacción (sequence diagrams, timing diagrams para expresar el
comportamiento en el tiempo), y por supuesto, los diagramas de máquinas de estado de UML,
formalizadas a partir de las máquinas de estado finito (FSM), también llamadas Autómatas finitos, que
son un tema esencial de la teoría de la computación, pues con ellos se representan modelos
matemáticos computacionales determinísticos y no determinísticos representando las entradas y salidas
de una máquina o autómata, que es una abstracción de una máquina que está en un solo estado en un
momento dado, y que ese estado puede variar (transicionar) a otro dependiendo de la entrada.

Precisamente la segunda parte de este trabajo recopila nuestra investigación en torno a Autómatas
Finitos Deterministas (DFAs), que constituyen una herramienta muy poderosa para describir el
procesamiento de máquinas computacionales. Constituye un antecedente para entender en un futuro
redes de Petri, las cuales están fuera del alcance de este trabajo, baste decir que las redes de Petri son
capaces de modelar (describir y analizar) sistemas distribuidos, y tienen toda una teoría matemática. Por
ello resulta fundamental la teoría de autómatas.

3
2 Investigación
2.1 Condiciones de carrera
2.1.1 Teoría, causas
Una condición de carrera puede ocurrir en sistemas concurrentes de distinta naturaleza: circuitos
eléctricos y electrónicos, programas multiproceso y programas multihilo, y claro también en sistemas
distribuidos, es decir, donde hay más de un proceso ejecutándose en paralelo en diferentes máquinas
que interactúan como parte de un mismo sistema. En este trabajo nos limitaremos a sistemas de
software.

La condición de carrera se detecta cuando el resultado (salida) del programa o sistema depende de
eventos (entrada) cuyo orden de ejecución no están realmente bajo nuestro control (Restrepo, s.f.), y las
ejecuciones en paralelo o concurrentes involucran un recurso compartido, y el diseñador del sistema o
el programador esperaba cierto orden de dichos eventos, tal que omitió sincronizar el acceso a dicho
recurso compartido.

Diferentes corridas del programa pueden tener resultados diferentes si realmente se prueba lo
suficiente, o bajo condiciones reales. La condición de carrera puede que ocurra de vez en cuando y se
identifique inicialmente como un error transitorio (Restrepo), o puede que prácticamente siempre
ocurra, sobre todo cuando dicho recurso compartido se usa con mucha frecuencia durante el
procesamiento. En cualquier caso, la salida del sistema depende de qué proceso o hilo de ejecución
llegó antes al recurso compartido, como si estuviese compitiendo contra otro proceso o varios, en una
carrera para ver quien llega primero, de ahí el término condición de carrera.

La salida del sistema puede parecer plausible (el proceso termina con un resultado como si fuese válido,
en vez de terminar indicando que ocurrió un error) pero al validar el resultado contra el esperado se
verá que es incorrecto o inconsistente con lo esperado, o puede dejar al sistema en un estado no
deseable, inválido.

Volver a correr el programa para depurarlo puede no ser muy útil, dado que no podemos controlar el
orden de los eventos, eso puede hacer todo un reto querer replicar el error. Y si es sensible al tiempo,
puede que ejecutarlo en una máquina de pruebas más lenta que la máquina que se usa para el entorno
de producción, el orden de los eventos tienda a ser diferente.

Así que para diagnosticar una condición de carrera, que implica analizar qué sucedió, sin duda puede
apoyarse de trazas en el log (si es que el programa genera un log), pero dada la dificultad de reproducir
el error, hace que el análisis del diseño mismo del algoritmo versus su implementación sea una mejor
opción para encontrar el error y arreglarlo.

2.1.1.1 Condiciones de Bernstein y secciones paralelizables


Recordemos que desde la etapa de análisis, el problema a resolver se puede programar de manera
concurrente si se identifican las secciones de código que pueden ser paralelizables, y son aquellas que
cumplan con las condiciones de Bernstein, que estipulan que se debe satisfacer que ninguno de los
bloques de código escribe lo que el otro bloque lee o escribe. En términos matemáticos:

Para un bloque de instrucciones 𝑆𝑘 a ejecutar, sean

4
• 𝐿(𝑆𝑘 ) el conjunto de variables de sólo Lectura.
• 𝐸(𝑆𝑘 ) el conjunto de variables que se Escriben.

Para paralelizar dos subconjuntos de instrucciones 𝑆𝑖 y 𝑆𝑗 se debe satisfacer que ninguno escribe lo que
el otro lee o escribe, es decir:

Condición Interpretación
𝐿(𝑆𝑖 ) ∩ 𝐸(𝑆𝑗 ) = ∅ Las variables que lee el proceso A, no son modificadas/escritas por el
proceso B en esa sección
𝐸(𝑆𝑖 ) ∩ 𝐿(𝑆𝑗 ) = ∅ Las variables que escribe/modifica el proceso A, no son leídas por el
proceso B en esa sección
𝐸(𝑆𝑖 ) ∩ 𝐸(𝑆𝑗 ) = ∅ Las variables que escribe el proceso A, no son sobre escritas por el
proceso B en esa sección

Las secciones que consisten de conjuntos de instrucciones no paralelizables, no cumplen con alguna de
las condiciones de Bernstein, y por tanto implica que debe programarse de tal manera que se asegure
que sólo un proceso las ejecuta (ejecución secuencial) o si varios procesos lo van a ejecutar, sólo uno
puede ejecutarlo a la vez, mientras otros esperan su turno de ejecución.

2.1.1.2 Secciones críticas


Cuando dicho conjunto de instrucciones involucra uno o más recursos compartidos entre procesos, se
denomina sección o región crítica. Por tanto, cuando se omite proteger esa sección, se sufrirá los
efectos indeseables de una condición de carrera.

Así que otra manera de definir una condición de carrera (race condition o race hazard) es el error en la
salida o comportamiento de un programa debido a una sección crítica que no se identificó como tal
desde un principio, y por ende no se aplicó la técnica apropiada para proteger dicha sección.

2.1.1.3 Race conditions por tipo de recursos compartidos


Los recursos compartidos pueden ser de diferente naturaleza, por ejemplo:

• Datos en memoria (data race): dos o más hilos compiten por usar un dato en memoria, y al
menos uno de ellos es para escribirlo/modificarlo. Esto puede ocurrir tanto con hilos como entre
procesos.
• Memoria del proceso, programación multihilo: donde el dato en memoria está en la
memoria del proceso, y todos los hilos tienen acceso al mismo espacio de direcciones en
memoria que el proceso que los crea.
• Memoria compartida, programación multiproceso: el data race puede ocurrir si el dato
reside en memoria compartida entre procesos. Esta memoria suele implementarse
como archivos mapeados en memoria (mmap files, memory-mapped files) y funciones
shm_* de POSIX por ejemplo, ver shm_overview (2022), o usando llamadas al sistema
equivalentes en sistemas operativos o plataformas que no soportan POSIX.
• Ejecución de APIs que no son thread-safe o thread-compatible (API race):
Diferentes hilos ejecutan al mismo tiempo una función, método (sea de un objeto, o estático, es
decir de la clase).

5
• Network race: el sistema llama dos web APIs de manera concurrente (o incluso protocolos de
red no web), el orden de las respuestas no necesariamente sigue el orden de invocación porque
los paquetes pueden experimentar latencias diferentes durante la transmisión vía red. Si el
sistema depende que lleguen en el mismo orden en que se llamaron, es de esperarse que sufra
de errores o inconsistencias debido a este tipo de race condition (ver bytefish, 2022).
• Archivos compartidos entre procesos: dos o más procesos pueden usar un archivo para
enviarse datos entre sí, sin importar si el archivo es local o remoto (sistema distribuido), un
proceso puede empezar a consumir el archivo y dejar de procesarlo siendo que el proceso
producer no ha terminado de escribir el archivo. La lectura del mismo debe estar sincronizado
con la escritura para evitar esa condición de carrera.
• Database race conditions: es un error común ignorar la concurrencia al diseñar queries, un dato
puede haber sido modificado por otro proceso para el momento en que se usa en cierto cálculo,
cuyo resultado ya no será válido. Por ello SQL soporta transacciones (ver Transactions, 2022),
como una unidad de trabajo con múltiples operaciones en la base, protegiendo secciones
críticas de datos compartidos vía la base de datos.

2.1.2 Código de ejemplo explicado


Para ilustrar el race condition sobre una variable global a todos los hilos que la usan, es decir un data
race, hemos creado el siguiente código en C++ donde una clase tiene una variable estática que se usa a
modo de contador. Y se crean 4 hilos, cada uno para incrementarlo 1 millón de veces, por lo que el
resultado final se espera que haya contado 4 millones. Se repartió la carga entre 4 hilos, a fin de acelerar
el cálculo. Veamos el código fuente:

// Materia: Programación Concurrente


// Actividad: A3
// Fecha: 2023-01-05
// Programa: main1_a3eq4
// Autor: Daniel Espinosa Jiménez
// Descripción: Ejemplo de race condition respecto a una variable
estática.
// La variable estática de una clase es global a todos sus
objetos.
// También conocido como data race.
// Se usa la clase thread portable que introduce C++11.
// La clase Counter es un Function Object.

#include <thread>
#include <iostream>

class Counter {
private:
static int count;
int MAX;
public:
Counter (const int & max)

6
: MAX(max)
{ }

void operator() () {
for (int i=0; i<MAX; ++i)
++count;
}
static int getCount() { return count; }
};
int Counter::count{0};

int main() {
std::cout << "Main thread: Launching 4 worker threads..." <<
std::endl;
const int max = 1000000;
std::thread thread1( (Counter(max)) );
std::thread thread2( (Counter(max)) );
std::thread thread3( (Counter(max)) );
std::thread thread4( (Counter(max)) );

std::cout << "Main thread: waiting for threads to finish..."<<


std::endl;
thread1.join();
thread2.join();
thread3.join();
thread4.join();

// Final counter should be 4,000,000


std::cout << "Main thread: Final counter = " << Counter::getCount()
<< " (It was expected: " << (max*4) << ")\n";

return 0;
}

Vemos que la sección crítica, representada aquí por el operador() de la clase Counter, no está protegida
contra data race. Como consecuencia de ello, en ninguna de las corridas se obtuvo el resultado
esperado, sino un número menor siempre diferente, tal y como se evidencia en la siguiente captura de
pantalla:

7
2.2 Interbloqueo
2.2.1 Teoría, causas
2.2.1.1 El problema de la exclusión mutua y los semáforos
Para explicar el interbloqueo, retomamos el problema de una race condition causado por no proteger
una sección crítica. Eso introduce la necesidad de un mecanismo para implementar la exclusión mutua,
un proceso o hilo bloquea el uso de un recurso mientras que los demás esperan a que lo libere.

De acuerdo con Zuberek (1999), Dijkstra fue quien ideó los semáforos como una solución al problema de
la exclusión mutua. El semáforo consiste de una variable integer a modo de contador, que sólo soporta
dos operaciones:

• P operation: prueba y decrementa.


• V operation: incrementa.

Un proceso en espera para usar un recurso, debe esperar a que el semáforo sea positivo, para poder
decrementarlo y continuar. Cuando termina de usar el recurso, debe ejecutar la operación V, de modo
que si hay algún proceso esperando el semáforo, también pueda continuar.

Se puede representar de la siguiente manera:

• Sean dos procesos, cada uno ejecutando una sección crítica que depende de la misma variable
compartida (cualquiera). Cada proceso puede usarla de modo diferente, uno puede estar
leyéndola y otro escribiéndola, o ambos escribiéndola.
• Sea var sem un semáforo, el cual puede ser manipulado con las funciones P() y V() descritas
arriba.

Entonces, el siguiente pseudocódigo muestra el uso del semáforo para proteger las secciones críticas:

8
Fig. 3. Solución del problema de la exclusión mutua usando semáforos. Tomado de Zuberek, 1999.

Ese ciclo que consta de una sección no crítica que podemos denominar NCS1 (non-critical section) para
el proceso 1, y NCS2 para el proceso 2, junto con las secciones críticas CS1 y CS2, y el semáforo como
mecanismo de sincronización compartido por ambos procesos, se puede representar como una red de
Petri de la siguiente manera:

Fig. 4. Red de Petri que modela la exclusión mutua usando semáforo. Tomado de Zuberek, 1999.

Nótese que al ser atómicas las operaciones P y V, no se puede ejecutar CS1 al mismo tiempo que CS2. La
red de Petri marca los estados con 𝑡1 , 𝑡2 , 𝑡3 , 𝑡4 para representar la secuencia de las operaciones
protegiendo de manera efectiva a las secciones críticas.

2.2.1.2 El problema del interbloqueo


Resulta que dicho mecanismo para bloquear y liberar recursos compartidos, bien puede que se use
incorrectamente, o que ocurran eventos inesperados y el proceso no libere el recurso como debía.

Así surge el problema del interbloqueo, el cual se refiere a una situación donde los procesos o hilos que
están compitiendo por recursos del sistema quedan todos bloqueados entre sí.

9
Sea 𝑃1 un proceso que usa un recurso 𝑅1 , y mientras lo usa está bloqueado para los demás procesos que
quieran usarlo al mismo tiempo, los cuales quedan en espera de que dicho recurso quede libre. Pero si
el proceso 𝑃1 intenta adquirir otro recurso 𝑅2 que está en uso y por tanto bloqueado por otro de los
procesos, sin liberar el recurso 𝑅1 , entonces 𝑃1 también queda bloqueado esperando. Así que todos los
procesos quedan bloqueados entre sí, el bloqueo es mutuo, por lo que también se conoce como
deadlock o candado muerto, o abrazo de la muerte.

Eso lo podemos representar de la siguiente manera (fig. 5) para dos procesos cuyas secciones críticas
usan las variables globales r1y r2, el proceso 1 está esperando el semáforo 𝑅2 para usar a la variable r2,
pero, asumiendo que se están ejecutando a la par, el proceso 2 ganó el semáforo R2, y viceversa, el
resultado neto para ambos, es que los dos procesos se quedan esperando sin poder ejecutar sus
secciones críticas.

Fig 5. Representación del interbloqueo asumiendo que las operaciones sobre los semáforos ocurren de
manera simultánea. Tomado de Zuberek, 1999.

Esta situación de deadlock la representa muy bien la siguiente figura:

10
Fig. 6. Representación de deadlock. Tomado
de Vallejo et al (2016).

Otra variante de este problema es dos


procesos o hilos que se comunican entre sí, y
deben seguir un protocolo, es decir cuando
uno escribe o manda un mensaje, el otro lo
recibe y lo lee. Pero si implementan mal el
protocolo, o se pierde de algún modo la
sincronía de sus estados, entonces pudiera ser
que los dos procesos se queden esperando
mutuamente a leer un mensaje que el otro
escriba.

2.2.2 Código de ejemplo explicado


Para ilustrar el interbloqueo, hemos creado el siguiente programa adaptando el programa multihilo del
ejemplo anterior de race condition. En esta adaptación, hemos usado dos mutex locks, a través de la
clase nativa std::mutex que introdujo C++11.

El primer mutex, es un mutex global para proteger el acceso a la salida standard, es decir para bloquear
el uso de std::cout que es llamado por el hilo main principal y los 4 hilos que crea aparte, es decir, hay
un total de 5 hilos.

El segundo mutex es un atributo estático de la clase Counter, ideado para proteger el uso del contador
estático count. Hasta aquí todo bien, pero hemos, a propósito, introducido dos errores que típicamente
ocasionan un candado muerto:

• El primer error es que el hilo main adquiere primero el mutex lock del std::cout, y luego el de
count cuando llama a getCount(), mientras que el operator() de la clase Counter primero
obtiene el lock del contador cout, y después el mutex del std::cout, es decir, se obtienen los
locks en orden inverso, lo cual es un error común.
• El segundo error es que en todas excepto una de las ocasiones que usamos un mutex, lo
hacemos a través de otro objeto, el std::lock_guard, el cual es la forma segura de usarlo, pues
cuando el objeto queda fuera de alcance se destruye, y su destructor siempre libera al mutex.
Eso evita errores del programador. Sin embargo, dentro de operator(), cuando obtiene el lock
para std::cout, se hace a través del método mutex.lock(), entonces depende del programador
liberar al lock después de usarlo, pero como se ve, lo libera dentro de un bloque if, por lo que no
siempre lo libera, y eso es otra forma de equivocarse y causar un dead lock.

Veamos el código fuente:

11
// Materia: Programación Concurrente
// Actividad: A3
// Fecha: 2023-01-06
// Programa: main2_a3eq4
// Autor: Daniel Espinosa Jiménez
// Descripción: Ejemplo de interbloqueo tras introducir mutex locks
// adaptando el ejemplo 1, para evitar el data race.
// Se usan las clases thread y mutex que introduce C++11.
// La clase Counter es un Function Object.

#include <thread>
#include <iostream>
#include <mutex>

// Global mutex to lock use of std::cout


std::mutex cout_mutex;

class Counter {
private:
static int count;
static std::mutex count_mutex;
int MAX;
public:
Counter (const int & max)
: MAX(max)
{ }

void operator() () {
for (int i=0; i<MAX; ++i) {
std::lock_guard guard1(count_mutex);
++count;
//std::lock_guard guard2(cout_mutex);
cout_mutex.lock();
if (i % (MAX/10) == 0) {
std::cout << "Thread ID " <<
std::this_thread::get_id() << ": Progress with i == " << i << std::endl;
cout_mutex.unlock();
}
}
}
static int getCount() {
std::lock_guard guard(count_mutex);
return count;
}
};

12
int Counter::count{0};
std::mutex Counter::count_mutex{};

int main() {
std::cout << "Main thread: Launching 4 worker threads..." <<
std::endl;
const int max = 1000000;
std::thread thread1( (Counter(max)) );
std::thread thread2( (Counter(max)) );
std::thread thread3( (Counter(max)) );
std::thread thread4( (Counter(max)) );

std::cout << "Main thread: waiting for threads to finish..."<<


std::endl;
int percent = 10;
for (int i =0; i<max; ++i) {
if (i%(max/10) == 0) {
std::lock_guard guard(cout_mutex);
std::cout << "Main thread progress = " << percent <<
std::endl;
percent += 10;
}
}
thread1.join();
thread2.join();
thread3.join();
thread4.join();

// Final counter should be 4,000,000


std::cout << "Main thread: Final counter = " << Counter::getCount()
<< " (It was expected: " << (max*4) << ")\n";

return 0;
}

La siguiente evidencia muestra que el dead lock hace que ya no avance ni el hilo main ni ninguno de los
los cuatro hilos contadores. Para hacerlo más evidente, hemos obtenido el id del proceso main2_a3eq4
que se queda bloqueado, y ejecutamos en otra terminal el comando top -H -p <pid> el cual muestra los
hilos de un proceso, así como su estado, y efectivamente muestra cinco hilos, todos en estado S
(suspended), como no están haciendo nada ninguno de estos hilos al quedarse bloqueados, su % de CPU
es 0.0.

13
2.3 Autómatas Finitos Deterministas (DFAs)
Los autómatas finitos determinísticos (AFD), son una herramienta esencial en el campo de la teoría de la
ciencia computacional, jugando un papel crucial en la construcción de algoritmos para el reconocimiento
de patrones y lenguaje. Dicho lo anterior, es necesario dejar en claro que los autómatas son máquinas de
estado finito que son definidos por un conjunto limitado de estados, un alfabeto de entrada y una función
de transición, que determina cómo es que el comportamiento de los estados se desarrollará en función
de las entradas mismas. El hecho de que sean determinísticos denota una de las características esenciales
de su funcionamiento, pues para cualquier combinación posible de entrada y estado, siempre hay una
única transición posible. Por lo tanto, es posible verlos aplicados por ejemplo en la validación de correos
electrónicos o la compilación de lenguajes de programación, pues los mismos procesan las secuencias de
entrada y determinan si es válida o no en función de las combinaciones aceptadas por el lenguaje dado.

Otra de las características básicas de este tipo de modelo computacional es que conforma uno de los
modelos más básicos en la teoría computacional y a su vez, contiene una memoria muy limitada. Así
mismo, es necesario comprender que, en este modelo matemático, existe una definición formal para
estos autómatas, la cual se ilustra de la siguiente manera con la tupla:

(𝑄,  Σ,  𝛿,  𝑞0 ,  𝐹)

En donde:

𝑄  =  𝐶𝑜𝑛𝑗𝑢𝑛𝑡𝑜 𝑑𝑒 𝑡𝑜𝑑𝑜𝑠 𝑙𝑜𝑠 𝑒𝑠𝑡𝑎𝑑𝑜𝑠 𝑝𝑜𝑠𝑖𝑏𝑙𝑒𝑠
Σ  =  𝐸𝑛𝑡𝑟𝑎𝑑𝑎𝑠
𝛿  =  𝐹𝑢𝑛𝑐𝑖𝑜𝑛 𝑑𝑒 𝑡𝑟𝑎𝑛𝑠𝑖𝑐𝑖𝑜𝑛 𝑑𝑎𝑑𝑎 𝑝𝑜𝑟 𝑄 𝑥 Σ  →  𝑄 
𝑞0   =  𝐸𝑠𝑡𝑎𝑑𝑜 𝑖𝑛𝑖𝑐𝑖𝑎𝑙

14
𝐹  =  𝐶𝑜𝑛𝑗𝑢𝑛𝑡𝑜 𝑑𝑒 𝑒𝑠𝑡𝑎𝑑𝑜𝑠 𝑓𝑖𝑛𝑎𝑙𝑒𝑠
Dicho lo anterior, los autómatas sueles representarse con ayuda de grafos, que ilustran la forma en la que
las entradas influyen en los diversos estados posibles y determinan el flujo que sigue la lógica del sistema.
Dichos grafos a su vez son conocidos como diagramas de transición. A continuación, se presentará el
ejemplo de un diagrama de transición y se explicarán sus partes correspondientes:

Imagen 1: Ejemplo de diagrama de transición.

En donde, la flecha verde indica el estado inicial, los estados están representados como los círculos en los
que es posible visualizar (e0, e1, e2, e3), y a su vez, el doble círculo de color azul, representa al estado final
del modelo. Por otra parte, las flechas marcadas con el número 0 o 1 son las posibles entradas que se
pueden dar en los diferentes estados y el flujo que seguirá el modelo en función de dicha entrada.

Con respecto al ejemplo mostrado, podemos determinar que:

𝑄  =  {𝑒0,  𝑒1,  𝑒2,  𝑒3}


Σ  =  {0,1}
𝑞0 =  𝑒0
𝐹  =  {𝑒3}
Siendo la función de transición (𝛿) representada en la siguiente tabla:

15
Imagen 2: | tabular de la función de transición.

La cual se obtiene básicamente de la representación de todos los estados posibles en función de las
entradas dadas. Es importante mencionar también que para que una combinación dada sea aceptada por
el autómata, ésta tiene que terminar en algún nodo que represente un estado final.

2.3.1 Ejercicio 1
Este ejercicio y el siguiente se tomaron de Fervari (2016).

Determine si las cadenas

𝑎𝑏𝑏𝑎𝑎 𝑎𝑏𝑏 𝑎𝑏𝑎 𝑎𝑏𝑎𝑎𝑎𝑎𝑎 𝑎𝑏𝑏𝑏𝑏𝑏𝑏𝑎𝑎𝑏


son aceptadas por el DFA definido por el siguiente diagrama

16
Fig. generada con GraphViz (PlantUML), ver Anexo 1.

Para resolver este primer ejercicio es necesario visualizar que como conjunto de posibles estados finales
tenemos que:

𝐹  =  {𝑞1,  𝑞2}, por lo tanto, resolviendo cada uno te los casos obtenemos como resultados:

1) 𝑎𝑏𝑏𝑎𝑎  =  𝑞0  →  𝑞1 → 𝑞0 → 𝑞0 → 𝑞1 → 𝑞2 . La cual si es aceptada ya que termina en 𝑞2 y el


mismo es un posible estado final.
2) 𝑎𝑏𝑏  =  𝑞0 → 𝑞1 → 𝑞0 → 𝑞0 . La cual no es aceptada ya que termina en 𝑞0 y el mismo no se
presenta como un posible estado final.
3) 𝑎𝑏𝑎  =  𝑞0 → 𝑞1 → 𝑞0 → 𝑞1 . La cual si es aceptada ya que termina en 𝑞1 y el mismo si es
aceptado como un posible estado final.
4) 𝑎𝑏𝑎𝑎𝑎𝑎𝑎  =  𝑞0 → 𝑞1 → 𝑞0 → 𝑞1 → 𝑞2 → 𝑞2 → 𝑞2 → 𝑞2 . La cual si es aceptada ya que
termina en 𝑞2 y el mismo si es aceptado como un posible estado final.
5) 𝑎𝑏𝑏𝑏𝑏𝑏𝑏𝑎𝑎𝑏  =  𝑞0 → 𝑞1 → 𝑞0 → 𝑞0 → 𝑞0 → 𝑞0 → 𝑞0 → 𝑞0 → 𝑞1 → 𝑞2 → 𝑞0 . La cual no es
aceptada ya que termina en 𝑞0 y el mismo no es aceptado como un posible estado final.

Dicho lo anterior a manera de resumen, podemos agrupar esta información en la siguiente tabla:

17
Imagen 3: Representación tabular de la aceptación de las cadenas dadas.

Si la cadena 𝜔 es aceptada, ¿toda subcadena de 𝜔 es aceptada?

No necesariamente, pues pueden existir subcadenas de 𝜔 que tengan como último elemento la entrada
𝑏 , lo cual invalidaría la subcadena.

¿Es aceptada la cadena 𝜔𝜔?

La concatenación 𝜔𝜔 es válida sí y solo sí el elemento final en el conjunto de entradas es =  𝛼 .

2.3.2 Ejercicio 2
Considere el autómata del ejercicio anterior. Justifique las siguientes afirmaciones:

• Si 𝜔 es aceptada, entonces termina en 𝑎.

Si, ya que las únicas maneras de llegar a alguno de los dos estados finales válidos: 𝐹  =  {𝑞1,  𝑞2}, es por
medio de un valor 𝛼 .

• Si 𝜔 termina en 𝑎, entonces es aceptada.

Si, pues sólo existen dos estados finales válidos 𝐹  =  {𝑞1,  𝑞2} los cuales solo son accesibles por medio
de un valor de entrada 𝛼 ya que todos los valores de entrada 𝑏 llevan al único estado final no válido: 𝑞0

(Ayuda: si 𝜔 = 𝛼𝑎, ¿dónde termino después de recorrer 𝛼? La sucesión de dicha cadena de la


de la siguiente forma: 𝑞0 → 𝑞1 → 𝑞2 ; Por lo tanto, terminó en 𝑞2 )

18
3 Conclusiones
Daniel Espinosa Jiménez

En este reporte de investigación hemos recordado los problemas de la programación concurrente, y


hemos descrito los diferentes tipos de race condition tales como data race y API race. Se dieron ejemplos
con diferentes tipos de recursos compartidos (archivos, base de datos, recursos en red, variables
compartidas) para mejorar la comprensión de este concepto.

Eso nos llevó a explicar qué es una sección crítica versus las secciones que son paralelizables, las
condiciones de Bernstein, y las descripciones se mejoraron ilustrando tanto con pseudocódigo como con
una red de Petri mostrando la exclusión mutua. La introducción hizo mención que las redes de Petri tienen
a las máquinas de estado como antecedente, y que son muy útiles para modelar las transiciones entre
estados de varios procesos en sistemas distribuidos.

Mientras que hay muchos tipos de diagramas como por ejemplo los de UML para diagramar actividades
y secuencias de mensajes, se resaltó el hecho en la introducción que las máquinas de estado o autómatas
finitos, junto con las redes de Petri, no solo permiten representar gráficamente un modelo computacional
de una solución a un problema, sino que además tienen toda una teoría matemática para su análisis y
diseño. Así es como la última parte de este reporte se enfoca en las bases de los autómatas finitos
deterministas, y se resuelven dos ejercicios con base a la teoría revisada, para empezar a adquirir
habilidades usando estas abstracciones, muy útiles para diseñar interpretadores léxicos como puede ser
parte de un sistema inteligente o un compilador.

Josafat Emanuel Díaz Martínez

En conclusión, la condición de carrera, el interbloqueo y los autómatas finitos determinísticos son


elementos clave en la programación concurrente. Principalmente tenemos que la condición de carrera es
un problema que surge cuando dos o más procesos compiten por acceder a los mismos recursos. Así
mismo, el interbloqueo ocurre cuando varios procesos están esperando mutuamente para acceder a los
recursos que necesitan. Y los autómatas finitos determinísticos pueden utilizarse para modelar y resolver
estos problemas de programación concurrente. En general, la comprensión y el manejo adecuado de estos
conceptos son críticos para el diseño de sistemas concurrentes eficientes y confiables. Evitando el mal
rendimiento o un uso inadecuado de los recursos temporales y espaciales que podrían desembocar en
una mala experiencia de usuario y un software bastante entorpecido.

Manuel de Jesús Mayorga González

La presencia de una condición de carrera o un interbloqueo en la sección crítica de un código puede


provocar graves problemas de rendimiento y estabilidad en el sistema. Es importante implementar
medidas para prevenir estas situaciones, como el uso de semáforos o monitores para garantizar el acceso
exclusivo a la sección crítica. También es importante realizar pruebas exhaustivas para identificar y
corregir cualquier posible problema antes de la implementación en producción.

19
Los autómatas finitos deterministas (AFD) son una herramienta fundamental en teoría de la computación
y se utilizan para describir y modelar el comportamiento de sistemas discretos y deterministas. Los AFD
son útiles para describir el reconocimiento de lenguajes y la solución de problemas formales. Debido a su
simplicidad y determinismo, los AFD son ampliamente utilizados en la implementación de compiladores,
analizadores sintácticos y otros sistemas de software. Sin embargo, es importante tener en cuenta que
los AFD tienen limitaciones y pueden no ser adecuados para describir sistemas más complejos que
involucran incertidumbre o naturaleza no determinista.

4 Referencias APA 7
C++11/C++14 9. DEADLOCKS – 2020 (2020).
https://www.bogotobogo.com/cplusplus/C11/9_C11_DeadLock.php

Espinosa, D., Díaz, J. E., Mayorga, M. de J. (30 de enero de 2023). A1. Mapa mental [Archivo PDF].
https://uvmonline.blackboard.com/ultra/courses/_140816_1/cl/outline

Fervari, R. (2016). Autómatas Finitos Deterministicos (DFA) [Archivo PDF].


https://cs.famaf.unc.edu.ar/~rfervari/icc16/slides/class-1-slides.pdf

Finite Automaton. (10 de mayo de 2021). https://graphviz.org/Gallery/directed/fsm.html

Finite-state machine. (24 de enero de 2023). https://en.wikipedia.org/wiki/Finite-state_machine


Khoussainov, B. (). Deterministic Finite Automata. Auckland: Computer Science Department, The
University of Auckland. Recuperado de:
https://www.cs.auckland.ac.nz/courses/compsci220s1t/lectures/lecturenotes/old/DFA.pdf

Lebedeva, K. (2016). Deterministic Finite Automata. COMP2600 – Formal Methods for Software
Engineering. [Diapositivas de PowerPoint]. Australian National University.
https://comp.anu.edu.au/courses/comp2600/Lectures/02DFA.pdf

MIT OpenCourseWare. (06 de Octubre de 2021). 1. Introduction, Finite Automata, Regular


Expressions.[Archivo de Video]. Youtube. https://www.youtube.com/watch?v=9syvZr-
9xwk&list=PLUl4u3cNGP60_JNv2MmK3wkOt9syvfQWY&index=1

Neso Academy. (17 de Diciembre de 2016). Deterministic Finite Automata (Example 1). [Archivo de
video]. Youtube.
https://www.youtube.com/watch?v=40i4PKpM0cI&list=PLBlnK6fEyqRgp46KUv4ZY69yXmpwKOIev&inde
x=4

Neso Academy. (23 de Diciembre de 2016). Finite State Machine (Finite Automata). [Archivo de video].
Youtube.
https://www.youtube.com/watch?v=Qa6csfkK7_I&list=PLBlnK6fEyqRgp46KUv4ZY69yXmpwKOIev&inde
x=3

Neso Academy. (15 de Diciembre de 2016). Finite State Machine (Prerequisite). [Archivo de video].
Youtube. https://www.youtube.com/watch?v=TpIBUeyOuv8

20
Neso Academy. (14 de Diciembre de 2016). Introduction to Theory of Computation. [Archivo de video].
Youtube. https://www.youtube.com/watch?v=58N2N7zJGrQ&list=PLzzWKvNpitf3IC7inf9FCYooIAjj4-2f-
&index=1&t=4s

Restrepo, F. (s.f.). Programación concurrente [Página web]. http://ferestrepoca.github.io/paradigmas-


de-programacion/progconcurrente/concurrente_teoria/index.html

rwestMSFT, et al. (18 de noviembre de 2022). Transactions (Transact-SQL).


https://learn.microsoft.com/en-us/sql/t-sql/language-elements/transactions-transact-sql?view=sql-
server-ver16

shm_overview. (18 de diciembre de 2022). https://man7.org/linux/man-


pages/man7/shm_overview.7.html

Stachecki, F. (8 de abril de 2004). Concurrency in UML [Archivo PDF]. https://www.omg.org/ocup-


2/documents/concurrency_in_uml_version_2.6.pdf

std::mutex with unique lock and lock guard c++11. (2023). https://digestcpp.com/cpp11/mutex/

Vallejo, D., González, C., Albusac, J. A. (2016). Programación concurrente y tiempo real [Archivo PDF].
3ra. Edición. http://190.57.147.202:90/xmlui/bitstream/handle/123456789/445/Programacion-
Concurrente-y-Tiempo-Real.pdf?sequence=1&isAllowed=y

Zuberek, W. M. (Octubre de 1999). Petri Net Models of Process Synchronization Mechanisms [Archivo
PDF]. http://www.cs.mun.ca/~wlodek/pdf/99-SMC-1.pdf

21
Anexo 1. Diagrama de máquina de estado
El diagrama del Ejercicio 1 es generado con el siguiente código de GraphViz, con PlantUML:

@startuml A3_Ejercicio1_DFA
' Materia: Programación Concurrente
' Tarea: Actividad 3.
' Fig del Ejercicio 1
' Autor: Daniel Espinosa Jiménez
' Fecha: 2023-02-03

digraph state_machine_M1 {
node [fontname="Helvetica,Arial,sans-serif"];
edge [fontname="Helvetica,sans-serif,"];
rankdir=RL;
node [shape = doublecircle]; q1, q2;
node [shape = circle ]; q0;
M1 [shape=none];
M1 -> q0
q0 -> q0 [label = "b"];
q0 -> q1 [label = a]
q1 -> q2 [label = a]
q2 -> q2 [label = "a"];
q2 -> q0 [label = "b"];
q1 -> q0 [label = "b"]

@enduml

22

También podría gustarte