Documentos de Académico
Documentos de Profesional
Documentos de Cultura
3.M54RFC |
A3. Reporte de
Programación Investigación
Concurrente
Condiciones de Carrera · Interbloqueo ·
Automátas Finitos determinísticos
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.
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.
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.
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.
• 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.
#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)) );
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:
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.
• 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.
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.
10
Fig. 6. Representación de deadlock. Tomado
de Vallejo et al (2016).
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.
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>
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)) );
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:
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:
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.
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).
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:
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.
No necesariamente, pues pueden existir subcadenas de 𝜔 que tengan como último elemento la entrada
𝑏 , lo cual invalidaría la subcadena.
2.3.2 Ejercicio 2
Considere el autómata del ejercicio anterior. Justifique las siguientes afirmaciones:
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, 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
18
3 Conclusiones
Daniel Espinosa Jiménez
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.
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
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
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
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