Download Introducción a Procesos y Programación Concurrente and more Study Guides, Projects, Research Computer science in PDF only on Docsity!
Capítulo 1
Conceptos Básicos
E
n este capítulo se plantean los aspectos básicos relativos al
concepto de proceso , estableciendo las principales diferen-
cias con respecto a una hebra o hilo y haciendo especial hin-
capié en su creación y gestión mediante primitivas POSIX. Así mismo,
también se establece un marco general para el estudio de los fun-
damentos de programación concurrente. Estos aspectos se discutirán
con más detalle en sucesivos temas.
Comunicando procesos
Los procesos necesitan algún tipo de mecanismo explícito tanto para compartir infor- mación como para sincroni- zarse. El hecho de que se ejecuten en una misma má- quina física no implica que los recursos se compartan de manera implícita sin proble- mas.
La problemática que se pretende abordar mediante el estudio de
la programación concurrente está vinculada al concepto de sistema
operativo multiproceso , donde los procesos comparten todo tipo de
recursos, desde la CPU hasta una impresora. Este planteamiento me-
jora la eficiencia del sistema, pero plantea la cuestión de la sincro-
nización en el acceso a los recursos. Típicamente, esta problemática
no se resuelve a nivel de sistema operativo, siendo responsabilidad del
programador el garantizar un acceso consistente a los recursos.
Este planteamiento general se introduce mediante el problema clá-
sico del productor/consumidor, haciendo hincapié en la necesidad de
compartir un buffer, y da lugar al concepto de sección crítica. Entre
las soluciones planteadas, destaca el uso de los semáforos y el paso
de mensajes como mecanismos clásicos de sincronización.
[ 2 ] CAPÍTULO 1. CONCEPTOS BÁSICOS
instr_
instr_
instr_
…
instr_i
…
instr_n
carga del ejecutable en memoria
programa proceso
datos
pila
memoria
texto
Figura 1.1: Esquema gráfico de un programa y un proceso.
1.1. El concepto de proceso
Informalmente, un proceso se puede definir como un programa en
ejecución. Además del propio código al que está vinculado, un proceso
incluye el valor de un contador de programa y el contenido de ciertos
registros del procesador. Generalmente, un proceso también incluye
la pila del proceso, utilizada para almacenar datos temporales, como
variables locales, direcciones de retorno y parámetros de funciones, y
una sección de datos con variables globales. Finalmente, un proceso
también puede tener una sección de memoria reservada de manera
dinámica. La figura 1.1 muestra la estructura general de un proceso.
1.1.1. Gestión básica de procesos
A medida que un proceso se ejecuta, éste va cambiando de un es-
tado a otro. Cada estado se define en base a la actividad desarrollada
por un proceso en un instante de tiempo determinado. Un proceso
puede estar en uno de los siguientes estados (ver figura 1.2):
Nuevo , donde el proceso está siendo creado.
En ejecución , donde el proceso está ejecutando operaciones o
instrucciones.
En espera , donde el proceso está a la espera de que se produzca
un determinado evento, típicamente la finalización de una opera-
ción de E/S.
Preparado , donde el proceso está a la espera de que le asignen
alguna unidad de procesamiento.
[ 4 ] CAPÍTULO 1. CONCEPTOS BÁSICOS
a fork() se genera una copia exacta que deriva en un nuevo proceso,
el proceso hijo, que recibe una copia del espacio de direcciones del
proceso padre. A partir de ese momento, ambos procesos continúan
su ejecución en la instrucción que está justo después de fork(). La
figura 1.3 muestra de manera gráfica las implicaciones derivadas de
la ejecución de fork() para la creación de un nuevo proceso.
fork() devuelve 0 al proceso hijo y el PID del hijo al padre.
Listado 1.2: La llamada fork() al sistema
1 #include <sys/types.h> 2 #include <unistd.h> 3 4 pid_t fork (void);
Independencia
Los procesos son indepen- dientes entre sí, por lo que no tienen mecanismos implíci- tos para compartir informa- ción y sincronizarse. Incluso con la llamada fork(), los pro- cesos padre e hijo son total- mente independientes.
Recuerde que fork() devuelve el valor 0 al hijo y el PID del hijo al
padre. Dicho valor permite distinguir entre el código del proceso pa-
dre y del hijo, con el objetivo de tener la posibilidad de asignar un
nuevo fragmento de código. En otro caso, la creación de dos procesos
totalmente idénticos no sería muy útil.
proceso_A
fork()
proceso_A
(cont.)
proceso_B
(hijo)
hereda_de
Figura 1.3: Creación de un proceso mediante fork().
1.1. El concepto de proceso [ 5 ]
El siguiente listado de código muestra un ejemplo muy básico de
utilización de fork() para la creación de un nuevo proceso. No olvide
que el proceso hijo recibe una copia exacta del espacio de direcciones
del proceso padre.
Listado 1.3: Uso básico de fork()
1 #include <sys/types.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 #include <stdio.h> 5 6 int main (void) { 7 int (^) *valor = malloc(sizeof(int)); (^8) *valor = 0; 9 fork(); (^10) *valor = 13; 11 printf(" %ld: %d\n", (long)getpid(), (^) *valor); 12 13 free(valor); 14 return 0; 15 }
La salida de este programa sería la siguiente^1 :
¿Cómo distinguiría el código del proceso hijo y el del padre?
Después de la ejecución de fork(), existen dos procesos y cada uno
de ellos mantiene una copia de la variable valor. Antes de su ejecución,
solamente existía un proceso y una única copia de dicha variable. Note
que no es posible distinguir entre el proceso padre y el hijo, ya que no
se controló el valor devuelto por fork().
Típicamente será necesario crear un número arbitrario de procesos,
por lo que el uso de fork() estará ligado al de algún tipo de estructura
de control, como por ejemplo un bucle for. Por ejemplo, el siguiente lis-
tado de código genera una cadena de procesos, tal y como se refleja de
manera gráfica en la figura 1.4. Para ello, es necesario asegurarse de
que el proceso generado por una llamada a fork(), es decir, el proceso
hijo, sea el responsable de crear un nuevo proceso.
(^1) El valor de los ID de los procesos puede variar de una ejecución a otra.
1.1. El concepto de proceso [ 7 ]
proceso_A
fork()
proceso_A
(cont.)
wait()
proceso_B
(hijo)
execl()
exit()
Limpieza
tabla proc
SIGCHLD
zombie
Figura 1.5: Esquema gráfico de la combinación fork()+exec().
El uso de operaciones del tipo exec implica que el proceso padre
tenga que integrar algún tipo de mecanismo de espera para la correcta
finalización de los procesos que creó con anterioridad, además de lle-
var a cabo algún tipo de liberación de recursos. Esta problemática se
discutirá más adelante. Antes, se estudiará un ejemplo concreto y se
comentarán las principales diferencias existentes entre las operacio-
nes de la familia exec, las cuales se muestran en el siguiente listado
de código.
Listado 1.5: Familia de llamadas exec
1 #include <unistd.h> 2 3 int execl (const char (^) *path, const char (^) *arg, ...); 4 int execlp (const char (^) *file, const char (^) *arg, ...); 5 int execle (const char (^) *path, const char (^) *arg, ..., 6 char (^) *const envp[]); 7 8 int execv (const char (^) *path, char (^) *const argv[]); 9 int execvp (const char (^) *file, char (^) *const argv[]); 10 int execve (const char (^) *path, char (^) *const argv[], 11 char (^) *const envp[]);
La llamada al sistema execl() tiene los siguientes parámetros:
1. La ruta al archivo que contiene el código binario a ejecutar por
el proceso, es decir, el ejecutable asociado al nuevo segmento de
código.
[ 8 ] CAPÍTULO 1. CONCEPTOS BÁSICOS
2. Una serie de argumentos de línea de comandos , terminado por
un apuntador a NULL. El nombre del programa asociado al proce-
so que ejecuta execl suele ser el primer argumento de esta serie.
La llamada execlp tiene los mismos parámetros que execl, pero
hace uso de la variable de entorno PATH para buscar el ejecutable
del proceso. Por otra parte, execle es también similar a execl , pero
añade un parámetro adicional que representa el nuevo ambiente del
programa a ejecutar. Finalmente, las llamadas execv difieren en la
forma de pasar los argumentos de línea de comandos, ya que hacen
uso de una sintaxis basada en arrays.
El siguiente listado de código muestra la estructura de un progra-
ma encargado de la creación de una serie de procesos mediante la
combinación de fork() y execl() dentro de una estructura de bucle.
Como se puede apreciar, el programa recoge por línea de órdenes el
número de procesos que se crearán posteriormente^2. Note cómo se ha-
ce uso de una estructura condicional para diferenciar entre el código
asociado al proceso padre y al proceso hijo. Para ello, el valor devuelto
por fork(), almacenado en la variable childpid, actúa de discriminante.
padre
h 1 h 2 ... hn
Figura 1.6: Representación gráfica de la creación de va- rios procesos hijo a partir de uno padre.
En el case 0 de la sentencia switch (línea
12 ) se ejecuta el código
del hijo. Recuerde que fork() devuelve 0 al proceso hijo, por lo que en
ese caso habrá que asociar el código del proceso hijo. En este ejemplo,
dicho código reside en una nueva unidad ejecutable, la cual se en-
cuentra en el directorio exec y se denomina hijo. Esta información es
la que se usa cuando se llama a execl(). Note cómo este nuevo proceso
acepta por línea de órdenes el número de procesos creados (argv[1]),
recogido previamente por el proceso padre (línea
Compilación
La compilación de los pro- gramas asociados al proceso padre y al hijo, respectiva- mente, es independiente. En otras palabras, serán objeti- vos distintos que generarán ejecutables distintos.
Listado 1.6: Uso de fork+exec
1 #include <sys/types.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 #include <stdio.h> 5 6 int main (int argc, char (^) *argv[]) { 7 pid_t childpid; 8 int n = atoi(argv[1]), i; 9 10 for (i = 1; i <= n; i++) 11 switch(childpid = fork()) { 12 case 0: // Código del hijo. 13 execl("./exec/hijo", "hijo", argv[1], NULL); 14 break; // Para evitar entrar en el for. 15 } 16 17 // Se obvia la espera a los procesos 18 // y la captura de eventos de teclado. 19 20 return 0; 21 }
(^2) Por simplificación no se lleva a cabo un control de errores
[ 10 ] CAPÍTULO 1. CONCEPTOS BÁSICOS
Listado 1.9: Uso de fork+exec+wait
1 #include <sys/types.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 #include <stdio.h> 5 #include <signal.h> 6 #include <wait.h> 7 8 #define NUM_HIJOS 5 9 10 void finalizarprocesos (); 11 void controlador (int senhal); 12 13 pid_t pids[NUM_HIJOS]; 14 15 int main (int argc, char (^) *argv[]) { 16 int i; 17 char (^) num_hijos_str; 18 num_hijos_str = (char)malloc(sizeof(int)); 19 sprintf(num_hijos_str, " %d", NUM_HIJOS); 20 21 // Manejo de Ctrol+C. 22 if (signal(SIGINT, controlador) == SIG_ERR) { 23 fprintf(stderr, "Abrupt termination.\n"); 24 exit(EXIT_FAILURE); 25 } 26 27 for (i = 0; i < NUM_HIJOS; i++) 28 switch(pids[i] = fork()) { 29 case 0: // Código del hijo. 30 execl("./exec/hijo", "hijo", num_hijos_str, NULL); 31 break; // Para evitar entrar en el for. 32 } 33 34 free(num_hijos_str); 35 36 // Espera terminación de hijos... 37 for (i = 0; i < NUM_HIJOS; i++) 38 waitpid(pids[i], 0, 0); 39 40 return EXIT_SUCCESS; 41 } 42 43 void finalizarprocesos () { 44 int i; 45 printf ("\n-------------- Finalizacion de procesos ------------- \n"); 46 for (i = 0; i < NUM_HIJOS; i++) 47 if (pids[i]) { 48 printf ("Finalizando proceso [ %d]... ", pids[i]); 49 kill(pids[i], SIGINT); printf("\n"); 50 } 51 } 52 53 void controlador (int senhal) { 54 printf("\nCtrl+c captured.\n"); printf("Terminating...\n\n"); 55 // Liberar recursos... 56 finalizarprocesos(); 57 exit(EXIT_SUCCESS); 58 }
1.1. El concepto de proceso [ 11 ]
El anterior listado muestra la inclusión del código necesario pa-
ra esperar de manera adecuada la finalización de los procesos hijo y
gestionar la terminación abrupta del proceso padre mediante la com-
binación de teclas Ctrl+C.
En primer lugar, la espera a la terminación de los hijos se con-
trola mediante las líneas
37-38 utilizando la primitiva waitpid. Esta
primitiva se comporta de manera análoga a wait, aunque bloquean-
do al proceso que la ejecuta para esperar a otro proceso con un pid
concreto. Note como previamente se ha utilizado un array auxiliar de-
nominado pids (línea
13 ) para almacenar el pid de todos y cada uno
de los procesos hijos creados mediante fork (línea
28 ). Así, sólo habrá
que esperarlos después de haberlos lanzado.
Por otra parte, note cómo se captura la señal SIGINT, es decir, la
terminación mediante Ctrl+C, mediante la función signal (línea
22 ), la
cual permite asociar la captura de una señal con una función de re-
trollamada. Ésta función definirá el código que se tendrá que ejecutar
cuando se capture dicha señal. Básicamente, en esta última función,
denominada controlador, se incluirá el código necesario para liberar
los recursos previamente reservados, como por ejemplo la memoria
dinámica, y para destruir los procesos hijo que no hayan finalizado.
Si signal() devuelve un código de error SIG_ERR, el programador es
responsable de controlar dicha situación excepcional.
Listado 1.10: Primitiva POSIX signal
1 #include <signal.h> 2 3 typedef void (*sighandler_t)(int); 4 5 sighandler_t signal (int signum, sighandler_t handler);
1.1.3. Procesos e hilos
Sincronización
Conseguir que dos cosas ocurran al mismo tiempo se denomina comúnmente sin- cronización. En Informática, este concepto se asocia a las relaciones existentes en- tre eventos, como la seriali- zación (A debe ocurrir antes que B) o la exclusión mutua (A y B no pueden ocurrir al mismo tiempo).
El modelo de proceso presentado hasta ahora se basa en un único
flujo o hebra de ejecución. Sin embargo, los sistemas operativos mo-
dernos se basan en el principio de la multiprogramación , es decir, en
la posibilidad de manejar distintas hebras o hilos de ejecución de ma-
nera simultánea con el objetivo de paralelizar el código e incrementar
el rendimiento de la aplicación.
Esta idea también se plasma a nivel de lenguaje de programación.
Algunos ejemplos representativos son los APIs de las bibliotecas de
hilos Pthread, Win32 o Java. Incluso existen bibliotecas de gestión
de hilos que se enmarcan en capas software situadas sobre la capa
del sistema operativo, con el objetivo de independizar el modelo de
programación del propio sistema operativo subyacente.
En este contexto, una hebra o hilo se define como la unidad básica
de utilización del procesador y está compuesto por los elementos:
Un ID de hebra , similar al ID de proceso.
1.1. El concepto de proceso [ 13 ]
Capacidad de respuesta , ya que el uso de múltiples hilos pro-
porciona un enfoque muy flexible. Así, es posible que un hilo se
encuentra atendiendo una petición de E/S mientras otro conti-
núa con la ejecución de otra funcionalidad distinta. Además, es
posible plantear un esquema basado en el paralelismo no blo-
queante en llamadas al sistema, es decir, un esquema basado en
el bloqueo de un hilo a nivel individual.
Compartición de recursos , posibilitando que varios hilos mane-
jen el mismo espacio de direcciones.
Eficacia , ya que tanto la creación, el cambio de contexto, la des-
trucción y la liberación de hilos es un orden de magnitud más
rápida que en el caso de los procesos pesados. Recuerde que las
operaciones más costosas implican el manejo de operaciones de
E/S. Por otra parte, el uso de este tipo de programación en ar-
quitecturas de varios procesadores (o núcleos) incrementa enor-
memente el rendimiento de la aplicación.
Caso de estudio. POSIX Pthreads
Pthreads es un estándar POSIX que define un interfaz para la crea-
ción y sincronización de hilos. Recuerde que se trata de una especifica-
ción y no de una implementación, siendo ésta última responsabilidad
del sistema operativo.
POSIX
Portable Operating System In- terface es una familia de es- tándares definidos por el co- mité IEEE con el objetivo de mantener la portabilidad en- tre distintos sistemas opera- tivos. La X de POSIX es una referencia a sistemas Unix.
El mánejo básico de hilos mediante Pthreads implica el uso de las
primitivas de creación y de espera que se muestran a continuación.
Como se puede apreciar, la llamada pthread_create() necesita una fun-
ción que defina el código de ejecución asociado al propio hilo (definido
en el tercer parámetro). Por otra parte, pthread_join() tiene un propó-
sito similar al ya estudiado en el caso de la llamada wait(), es decir, se
utiliza para que la hebra padre espera a que la hebra hijo finalice su
ejecución.
Listado 1.11: Primitivas POSIX Pthreads
1 #include <pthread.h> 2 3 int pthread_create (pthread_t (^) *thread, 4 const pthread_attr_t (^) *attr, 5 void (^) (start_routine) (void (^) *), 6 void (^) *arg); 7 8 int pthread_join (pthread_t thread, void (^) **retval);
El listado de código de la página siguiente muestra un ejemplo muy
sencillo de uso de Pthreads en el que se crea un hilo adicional que tie-
ne como objetivo realizar el sumatorio de todos los números inferiores
o iguales a uno pasado como parámetro. La función mi_hilo() (líneas
27-36 ) es la que realmente implementa la funcionalidad del hilo crea-
do mediante pthread_create() (línea
19 ). El resultado se almacena en
una variable definida globalmente. Para llevar a cabo la compilación y
[ 14 ] CAPÍTULO 1. CONCEPTOS BÁSICOS
ejecución de este ejemplo, es necesario enlazar con la biblioteca pth-
reads:
$ gcc -lpthread thread_simple.c -o thread_simple $ ./thread_simple
El resultado de la ejecución de este programa para un valor, dado
por línea de órdenes, de 7 será el siguiente:
$ Suma total: 28.
Listado 1.12: Ejemplo de uso de Pthreads
1 #include <pthread.h> 2 #include <stdio.h> 3 #include <stdlib.h> 4 5 int suma; 6 void (^) *mi_hilo (void (^) *valor); 7 8 int main (int argc, char (^) *argv[]) { 9 pthread_t tid; 10 pthread_attr_t attr; 11 12 if (argc != 2) { 13 fprintf(stderr, "Uso: ./pthread \n"); 14 return -1; 15 } 16 17 pthread_attr_init(&attr); // Att predeterminados. 18 // Crear el nuevo hilo. 19 pthread_create(&tid, &attr, mi_hilo, argv[1]); 20 pthread_join(tid, NULL); // Esperar finalización. 21 22 printf("Suma total: %d.\n", suma); 23 24 return 0; 25 } 26 27 void (^) *mi_hilo (void (^) *valor) { 28 int i, ls; 29 ls = atoi(valor); 30 i = 0, suma = 0; 31 32 while (i <= ls) 33 suma += (i++); 34 35 pthread_exit(0); 36 }
[ 16 ] CAPÍTULO 1. CONCEPTOS BÁSICOS
while (1) {
// Produce en nextP.
while (cont == N);
// No hacer nada.
buffer[in] = nextP;
in = (in + 1) % N;
cont++;
while (1) {
while (cont == 0);
// No hacer nada.
nextC = buffer[out];
out = (out + 1) % N;
cont--;
// Consume nextC.
r
11
= cont
r 1 = r 1 + 1
cont = r
1
r
22
= cont
r 2 = r 2 - 1
cont = r
2
cont++
cont--
p: r
1
= cont
p: r 1 = r 1 + 1
c: r
2
= cont
c: r 2 = r 2 - 1
p: cont = r 1
c: cont = r 2
Figura 1.8: Código para los procesos productor y consumidor.
1.2.2. La sección crítica
El segmento de código en el que un proceso puede modificar va-
riables compartidas con otros procesos se denomina sección crítica
(ver figura 1.9). Para evitar inconsistencias, una de las ideas que se
plantean es que cuando un proceso está ejecutando su sección crítica
ningún otro procesos puede ejecutar su sección crítica asociada.
El problema de la sección crítica consiste en diseñar algún tipo de
solución para garantizar que los procesos involucrados puedan operar
sin generar ningún tipo de inconsistencia. Una posible estructura para
abordar esta problemática se plantea en la figura 1.9, en la que el
código se divide en las siguientes secciones:
Sección de entrada , en la que se solicita el acceso a la sección
crítica.
Sección crítica , en la que se realiza la modificación efectiva de
los datos compartidos.
Sección de salida , en la que típicamente se hará explícita la sa-
lida de la sección crítica.
Sección restante , que comprende el resto del código fuente.
Normalmente, la sección de entrada servirá para manipular algún
tipo de mecanismo de sincronización que garantice el acceso exclusivo
a la sección crítica de un proceso. Mientras tanto, la sección de sa-
lida servirá para notificar, mediante el mecanismo de sincronización
1.2. Fundamentos de programación concurrente [ 17 ]
SECCIÓN_ENTRADA
SECCIÓN_SALIDA
SECCIÓN_CRÍTICA
SECCIÓN_RESTANTE
do {
} while (1);
Figura 1.9: Estructura general de un proceso.
correspondiente, la salida de la sección crítica. Este hecho, a su vez,
permitirá que otro proceso pueda acceder a su sección crítica.
Cualquier solución al problema de la sección crítica ha de satisfacer
los siguientes requisitos:
1. Exclusión mutua , de manera que si un proceso pi está en su
sección crítica, entonces ningún otro proceso puede ejecutar su
sección crítica.
2. Progreso , de manera que sólo los procesos que no estén en su
sección de salida, suponiendo que ningún proceso ejecutase su
sección crítica, podrán participar en la decisión de quién es el
siguiente en ejecutar su sección crítica. Además, la toma de esta
decisión ha de realizarse en un tiempo limitado.
3. Espera limitada , de manera que existe un límite en el número
de veces que se permite entrar a otros procesos a sus secciones
críticas antes de que otro proceso haya solicitado entrar en su
propia sección crítica (y antes de que le haya sido concedida).
Las soluciones propuestas deberían ser independientes del número
de procesos, del orden en el que se ejecutan las instrucciones máqui-
nas y de la velocidad relativa de ejecución de los procesos.
Una posible solución para N procesos sería la que se muestra en el
siguiente listado de código. Sin embargo, esta solución no sería válida
ya que no cumple el principio de exclusión mutua , debido a que un
proceso podría entrar en la sección crítica si se produce un cambio de
contexto justo después de la sentencia while (línea
1.2. Fundamentos de programación concurrente [ 19 ]
Listado 1.15: Solución 2 procesos con array de booleanos
1 do { 2 // SECCIÓN DE ENTRADA 3 flag[i] = true; 4 while (flag[j]); // No hacer nada. 5 // SECCIÓN CRÍTICA. 6 // ... 7 // SECCIÓN DE SALIDA. 8 flag[i] = false; 9 // SECCIÓN RESTANTE. 10 }while (1);
¿Qué ocurre si se invierte el orden de las operaciones de la SE?
Listado 1.16: Solución 2 procesos con array de booleanos
1 do { 2 // SECCIÓN DE ENTRADA 3 while (flag[j]); // No hacer nada. 4 flag[i] = true; 5 // SECCIÓN CRÍTICA. 6 // SECCIÓN DE SALIDA. 7 flag[i] = false; 8 // SECCIÓN RESTANTE. 9 }while (1);
Esta solución no cumpliría con el principio de exclusión mutua ,
ya que los dos procesos podrían ejecutar su sección crítica de manera
concurrente.
Solución de Peterson (2 procesos)
En este apartado se presenta una solución al problema de la sin-
cronización de dos procesos que se basa en el algoritmo de Peterson,
en honor a su inventor, y que fue planteado en 1981. La solución de
Peterson se aplica a dos procesos que van alternando la ejecución de
sus respectivas secciones críticas y restantes. Dicha solución es una
mezcla de las dos soluciones propuestas anteriormente.
Los dos procesos comparten tanto el array flag, que determina los
procesos que están listos para acceder a la sección crítica, como la
variable turn, que sirve para determinar el proceso que accederá a su
sección crítica. Esta solución es correcta para dos procesos, ya que
satisface las tres condiciones anteriormente mencionadas.
Para entrar en la sección crítica, el proceso pi asigna true a flag[i] y
luego asigna a turn el valor j, de manera que si el proceso pj desea en-
trar en su sección crítica, entonces puede hacerlo. Si los dos procesos
[ 20 ] CAPÍTULO 1. CONCEPTOS BÁSICOS
intentan acceder al mismo tiempo, a la variable turn se le asignarán
los valores i y j (o viceversa) en un espacio de tiempo muy corto, pe-
ro sólo prevalecerá una de las asignaciones (la otra se sobreescribirá).
Este valor determinará qué proceso accederá a la sección crítica.
Listado 1.17: Solución 2 procesos; algoritmo de Peterson
1 do { 2 // SECCIÓN DE ENTRADA 3 flag[i] = true; 4 turn = j; 5 while (flag[j] && turn == j); // No hacer nada. 6 // SECCIÓN CRÍTICA. 7 // ... 8 // SECCIÓN DE SALIDA. 9 flag[i] = false; 10 // SECCIÓN RESTANTE. 11 }while (1);
A continuación se demuestra que si p 1 está en su sección crítica,
entonces p 2 no está en la suya.
- p1 en SC (premisa)
- p1 en SC --> flag[1]=true y (flag[2]=false o turn!=2) (premisa)
- flag[1]=true y (flag[2]=false o turn!=2) (MP 1,2)
- flag[2]=false o turn!=2 (A o B) (EC 3)
Demostrando A
- flag[2]=false (premisa)
- flag[2]=false --> p2 en SC (premisa)
- p2 en SR (p2 no SC) (MP 5,6)
Demostrando B
- turn=!2 (premisa)
- flag[1]=true (EC 3)
- flag[1]=true y turn=1 (IC 8,9)
- (flag[1]=true y turn=1) --> p2 en SE (premisa)
- p2 en SE (p2 no SC) (MP 10,11)
Solución de Lamport (n procesos)
El algoritmo para n procesos desarrollado por Lamport es una solu-
ción general e independiente del número de procesos inicialmente con-
cebida para entornos distribuidos. Los procesos comparten dos arrays,
uno de booleanos denominado eleccion, con sus elementos inicializa-
dos a false, y otro de enteros denominado num, con sus elementos
inicializados a 0.
Este planteamiento se basa en que si pi está en la sección crítica y
pj intenta entrar, entonces pj ejecuta el segundo bucle while y detecta
que num[i] 6 = 0, garantizando así la exclusión mutua.