1. PONTIFICIA UNIVERSIDAD CATÓLICA DEL PERÚ
FACULTAD DE CIENCIAS E INGENIERÍA
LABORATORIO DE ARQUITECTURA DE COMPUTADORAS
(IEE208)
LABORATORIO N° 5
TEMA: PROCESOS Y THREADS
2015-2
2. LABORATORIO 5: GUÍA TEÓRICA
PROCESOS Y THREADS
Threads y procesos son conceptos ampliamente usados en programación concurrente. Su
importancia y uso permite la realización de procesamiento en paralelo, optimización en el
uso de recursos de hardware para aplicaciones de procesamiento de grandes volúmenes
de datos y para realización y uso de sistemas multitarea. A continuación se presentan
brevemente tanto sus conceptos como principales características.
Procesos
Un proceso puede ser definido, en líneas generales, como un programa que se está
ejecutando, identificado con un Process ID, el cual es una suerte identificador (PID). Sin
embargo, un proceso también comprende los recursos que usa el programa en ejecución,
por ejemplo, un proceso cualquiera engloba:
a) El programa en ejecución.
b) Datos del programa almacenados en los sectores de heap y stack.
c) Variables guardadas en registros.
d) Contador PC del programa: contador que apunta a la dirección de la siguiente
instrucción.
Tener en cuenta que toda la información comprendida en un proceso es dinámica: las
variables pueden cambiar su valor, un programa puede reservar más memoria y liberar
memoria a lo largo de su ejecución.
Los procesos son asignados a procesadores, recursos para ejecución, para su ejecución,
sin embargo, típicamente hay más procesos que procesadores. Estos procesadores
“multiplexan” la ejecución para lograr que varios procesos se ejecuten, utilizando para
ello, un esquema de planificación como el Round Robin. Siempre que un proceso detenga
su ejecución para continuar o iniciar la ejecución de otro proceso, el estado actual del
proceso a detenerse se guarda para la continuación de su ejecución.
Un proceso siempre debe obtener datos para su ejecución. En sistemas UNIX, la función
fork()es usada para crear un proceso P2 a partir de un proceso P1, donde P2 es una
copia idéntica a P1 en el momento de la llamada a la función fork(). El proceso hijo (P2)
obtiene un Process ID diferente al proceso padre (P1), trabaja en una copia del espacio
de direcciones asignado al proceso padre y estos ejecutan el mismo programa,
empezando por la instrucción siguiente a fork(), sin embargo, dado que los procesos
padre e hijo tienen un Process ID diferente, pueden ejecutar intrucciones diferentes. Es
importante recalcar que copiar los datos necesarios y la copia del proceso en sí es una
operación de alto costo en tiempo.
A continuación se presenta un código en C que utiliza la función fork() de Linux para
crear un proceso hijo:
#include <stdio.h>
#include <sys/types.h>
4. Threads
Los threads o hilos de programación son unidades de trabajo mínimas compuestas por
una secuencia de instrucciones que pueden ser controladas por un scheduler
(planificador) y que comparten un espacio de direcciones, si es que están asociados a un
mismo proceso. Usualmente, un proceso está compuesto por 1 o más threads. Al igual
que los procesos, cada thread tiene asignado recursos de ejecución mediante
planficación.
De la misma forma que los procesos, su ejecución puede ser concurrente dependiendo de
los recursos del hardware. Usualmente, si la plataforma en donde se está trabajando está
basada en multiprocesadores (procesadores multicore como Intel i7, algunos
microcontroladores ARM, etc.), es altamente probable que la ejecución tanto de threads
como procesos sea concurrente.
Visibilidad de datos
Threads asociados a un mismo proceso comparten el mismo espacio de direcciones, es
decir, pueden acceder a los datos “compartidos” almacenados en las direcciones de ese
espacio: Variables globales y objetos dedatos dinámicamente localizados. Sin embargo,
por cada thread hay un espacio en la pila para variables locales y control de llamada de
funciones. Este espacio no es accesible por todos los threads sino solo por el thread
asociado y solosobrevive hasta el término del thread asociado.
Sincronización
El acceso de un thread a variables globables tiene algunos incovenientes. Por ejemplo, si
varios threads intentan escribir en el mismo espacio de memoria se tendrá un race
condition, y el resultado almacenado en ese espacio de memoria sería incierto. Para
evitar estos inconvenientes, en el modelo de threads, se ha introducido el concepto de
sincronización, para la cual se han introducido varias herramientas, entre ellas, destacan:
1. Mutexes
Exclusión Mutua . Son variables que indican una cerradura, para lo cual solo tienen 2
estados posibles: locked (cerrado) y unlocked (liberado). Para ello se usan 2 operaciones:
lock(var) y unlock(var) para cerrar y liberar la variable var. Si un thread cierra una
variable, entonces no es posible que un thread pueda realizar operación alguna hasta que
el thread que cerró la variable la libere. Su uso puede llegar a secuencializar o serializar la
ejecución de los threads, en otras palabras, la ejecución de los threads deja de ser
concurrente.
2. Semaphores (semáforos)
Son estructuras de datos que contienen contadores de tipo entero s, los cuales pueden
alterados usando las operaciones wait(s) y signal(s). La operación wait(s) espera a que el
contador s sea mayor que 0, si este es el caso, decrementa s en 1, mientras que la
operación signal(s), incrementa el valor de s en 1. Considerando que exista una sección
crítica cuya ejecución debe ser controlada (un thread a la vez), el funcionamiento de los
semáforos teniendo en cuenta que un thread tiene la siguiente secuencia de
instrucciones:
5. Un thread puede acceder a la sección crítica después de ejecutar la función wait(s), y
este tendrá acceso exclusivo a este hasta que ejecute signal(s). Cualquier otro thread
es bloqueado cuando ejecuta wait(s) y queda en espera hasta que el thread
propietario de la sección crítica ejecute signal(s).
Deadlocks
Un uso inadecuado de herramientas de sincronización puede llevar a un deadlock, es
decir, a un bloqueo mutuo entre threads. Un ejemplo de este bloque es el siguiente: Un
thread espera un evento que solo puede ser hecho por otro thread que también está
esperando un evento que solo puede ser realizado por el thread que también está
esperando.
Ejemplo:
Thread T1 Thread T2
Thread T1 realiza un lock a la variable s1, luego trata de hacer lock a la variable s2.
Thread T2 realiza un lock a la variable s2, luego trata de hacer lock a la variable s1.
T1 espera que la variable s2 sea liberada (released), mientras que T2 espera que la
variable s1 sea liberada, sin embargo, solo T1 puede liberar s1 y solo T2 puede liberar s2,
por lo cual se ha llegado a un deadlock.
1: lock(s1);
2: lock(s2);
3: do work();
4: unlock(s2);
5: unlock(s1);
1: lock(s2);
2: lock(s1);
3: do work();
4: unlock(s1);
5: unlock(s2);
1: wait(s);
2: critical section
3: signal(s);
6. THREADING Y MULTIPROCESSING EN PYTHON
Python ofrece soporte para realizar programación en threads y procesos. Varios módulos
han sido implementados para el uso y control de threads y procesos en sistemas UNIX y
Linux. Algunos de ellos son o están basados en MPI (Message Passing Interface),
OpenMP (Open Message Passing), en el caso de UNIX pthreads (Posix threads), entre
otros. En esta oportunidad, se explicarán conceptos básicos y ejemplos usando los
módulos threading y multiprocessing.
threading
La librería estándar de Python incluye soporte para hilos o threads. Conviene mencionar
que Python hace uso de un Global Interpreter Lock (GIL), un mecanismo usado por el
intérprete para sincronizar la ejecución de hilos, que básicamente consiste en que
solamente un hilo sea ejecutado al mismo tiempo, haciendo uso de los recursos
asignados a un solo procesador. Es debido a esto que la librería multithreading no debe
ser usada para propósitos de paralelización en general.
Un hilo accede a los distintos objetos en Python sosteniendo un lock. Por medio de la
función sys.setcheckinterval(check_interval) se define la frecuencia con la cual el
intérprete libera y reacomoda el lock para que más de un hilo pueda usar los recursos.
Esta frecuencia es definida por el argumento check_interval, cuyo valor por defecto es
100. Esto significa que cada 100 instrucciones virtuales de Python, el intérprete buscará
cambiar el hilo que tiene lock. Si el valor es menor a cero, la revisión se hace por cada
instrucción virtual, lo cual mejora la sensibilidad pero también el trabajo (overhead).
A continuación se muestra un ejemplo hecho con la libería threading:
#!usr/bin/python
# declaracion de modulos
import threading
import datetime
import time
# clases
class Hilo(threading.Thread):
def run(self):
now = datetime.datetime.now()
string = self.getName(),' te saluda en el tiempo ', now
print string
# Programa principal
for i in range(2): # Crear dos hilos
t = Hilo()
t.start()
time.sleep(1)
7. Este ejemplo crea 2 hilos, donde cada 1 imprime una cadena de caracteres donde indica
su nombre y el tiempo en el que se ejecuta el print.
Más información: https://docs.python.org/2/library/threading.html
8. Multiprocessing
Este módulo de Python permite la ejecución de varios procesos en simultáneo. En este
caso, Python evita hacer uso del Global Interpreter Lock, permitiendo que los procesos
sean ejecutados haciendo uso de todos los procesadores del CPU, a diferencia del
módulo threading. Esto permite que esta librería sea usada para realizar computación en
paralelo.
Variables compartidas
El siguiente ejemplo muestra el uso de variables compartidas, las cuales se declaran
usando Array o Value. La función Process() se utiliza para declarar un objeto y poner en
ejecución a un proceso hijo desde un proceso padre con el método start().
#!usr/bin/python
from multiprocessing import Process, Value, Array
def f(n, a):
n.value = 3.1415927
for i in range(len(a)):
a[i] = a[i]
if __name__ == '__main__':
num = Value('d', 0.0)
arr = Array('i', range(10))
p = Process(target=f, args=(num, arr))
p.start()
p.join()
print num.value
print arr[:]
Sincronización
La función Lock() se usa para declarar un objeto para realizar sincronización usando
mutexes. El objeto declarado tiene 2 métodos: acquire() y release() los cuales
realiza el lock y el unlock respectivamente. El siguiente ejemplo muestra el uso de la
función lock para sincronizar 10 procesos y secuencializar su ejecución.
#!usr/bin/python
from multiprocessing import Process, Lock
def f(l, i):
l.acquire()
print 'hello world', i
l.release()
if __name__ == '__main__':
lock = Lock()
9. for num in range(10):
Process(target=f, args=(lock, num)).start()
Lanzar varios procesos en paralelo
En el siguiente ejemplo se muestra el uso de la función Pool() para lanzar varios
procesos en paralelo y provechar el paralelismo de datos.
from multiprocessing import Pool
def f(x):
return x*x
if __name__ == '__main__':
p = Pool(5)
print(p.map(f, [1, 2, 3]))
Más información: https://docs.python.org/2/library/multiprocessing.html