Procesos
Concepto y ciclo de vida
- El ciclo de vida de un proceso es el siguiente:
-
El encargado de darle la oportunidad de usar la CPU es el Planificador de procesos o Scheduler → forma parte del núcleo del SO. En este otro enlace tienes más información sobre el planificador del núcleo Linux.
-
Y en este vídeo puedes ver que puede pasar si un atacante se puede hacer con el control del planificador.
-
Una forma bastante justa y extendida es hacerlo mediante asignación de rodajas de tiempo:
- Cuando un proceso cumple su tiempo de permanencia en el procesador, éste es desalojado y pasado a Listo. Esperará una nueva oportunidad para pasar a ejecución. También puede abandonar voluntariamente la CPU
-
El acto de cambiar un proceso de estado se llama Cambio de contexto. Se trata de una operación costosa
-
En un SO tradicional, la memoria se divide en:
- Espacio de usuario: en él se encuentra la mayor parte de la información relativa a los procesos de usuario
- Espacio de núcleo: en él reside el código y las estructuras propias del sistema operativo.
-
La información relativa a un proceso suele estar dividida entre los dos espacios.
-
La parte del espacio del núcleo contiene lo que se conoce como bloque de control del proceso
Procesos en Unix
-
En Unix todos los procesos, excepto el primero (el número
0
), se crean con una llamada afork()
. Puedes ver un contraargumento al uso defork
en este artículo. Puedes ver toda la información relativa afork
con la orden:man 2 fork
o también aquí. -
Para optimizar la creación del nuevo proceso se emplean técnicas como Copy On Write (COW).
-
El proceso que invoca a
fork
es el proceso padre. -
El proceso creado es el proceso hijo.
-
El proceso
0
se crea en el arranque. Hace una llamada afork
para crear el proceso1
,init
, y a continuación se convierte en el proceso “intercambiador de procesos”. -
Podemos observar la tabla de procesos activos con instrucciones como
top
,htop
,ps
, etc…
Figure 2: Ciclo de vida.
int pid;
if ( (pid = fork()) == -1 )
perror ("Error en la llamada a fork");
else if (pid == 0)
// código que ejecutará el proceso hijo
else
// código que ejecutará el proceso padre
-
La llamada a fork duplica todo el contexto del proceso. Es interesante que conozcas el uso idiomático de
fork-exec
. Para todo ello, echa un vistazo a este vídeo. -
Todas las variables, incluidas las globales y las estáticas, son inaccesibles para el otro proceso: compartir información es complicado (mecanismos Inter-Process Comunication, IPC)
Ejemplo:
/************/ /* procesos */ /************/ #include <sys/types.h> #include <unistd.h> #include <sys/wait.h> #include <stdlib.h> #include <stdio.h> #define NUM_PROCESOS 5 int I = 0; void codigo_del_proceso (int id) { int i; for (i = 0; i < 50; i++) printf("Proceso %d: i = %d, I = %d\n", id, i, I++); exit(id); // el id se almacena en los bits 8 al 15 antes de // devolverlo al padre } int main() { int p; int id [NUM_PROCESOS] = {1,2,3,4,5}; int pid; int salida; for (p = 0; p < NUM_PROCESOS; p++) { pid = fork (); if (pid == -1) { perror ("Error al crear un proceso: "); exit (-1); } else if (pid == 0) // Codigo del hijo codigo_del_proceso (id[p]); } // Codigo del padre for (p = 0; p < NUM_PROCESOS; p++) { pid = wait (&salida); printf("Proceso %d con id = %x (%x) terminado\n", pid, salida >> 8, WEXITSTATUS(salida)); } }
Hilos
Concepto y ciclo de vida
-
Los hilos permiten concurrencia dentro de cada proceso
-
Los procesos son entidades pesadas
- la estructura del proceso está en la parte del núcleo, y cada vez que un proceso quiere acceder a ella tiene que hacer una llamada al sistema y consumir tiempo de procesador
-
Los hilos son entidades ligeras: la estructura de hilos reside en el espacio de usuario.
- Los hilos comparten la información del proceso, por lo que si un hilo modifica una variable de proceso, el resto de hilos verán esa modificación cuando accedan a esa variable.
- El cambio de contexto entre hilos consume poco tiempo de procesador, de ahí su éxito.
Tipos y niveles de hilos
Hilos y hardware
Figure 3: Hilos y hardware.
Niveles de hilos
- Hoy en día es normal que nos encontremos con dos ’niveles’ de
hilos
- El que nos proporcione el lenguaje de programación empleado, p.e. Java
- El que proporciona el SO.
- Lo habitual es que el primero se reescriba mediante llamadas a este segundo -uno a uno, muchos a uno, muchos a muchos-.
Procesos ligeros
-
Hoy en día los SO ofrecen el concepto de proceso ligero (LWP: Light Weight Process).
-
Un LWP se ejecuta en espacio de usuario y esta sustentado por un thread o hilo.
-
Un LWP comparte su espacio de direcciones y recursos del sistema con otros LWP que pueda crear el mismo proceso.
-
Asociado al concepto de
hilo
aparece el de almacenamiento local al hilo -Thread Local Storage- oTLS
.-
Lenguajes como D lo usan por defecto.
-
En el caso de
C++
debemos emplearC++11
o superior para tener soporteTLS
. -
En el caso de Rust disponemos de la macro
thread_local!
-
Y en Java tenemos la clase ThreadLocal
.
-
Implementación de hilos
Hilos en Unix con C (POSIX)
-
Se emplea la biblioteca
pthread
o POSIX threads. -
Es el interfaz más utilizado para implementar bibliotecas de hilos en entornos Unix.
#include <pthread.h> int pthread_create(...); // crear hilo pthread_t pthread_self(void); // Devuelve el ID dle hilo actual void pthread_exit(...); // terminar hilo int pthread_join(...); // espera por otro hilo int pthread_equal(...); // comprueba si dos hilos son el mismo
-
Los compiladores actuales de C/C++ como los de los proyectos GCC y LLVM incluyen lo que llaman desinfectantes de distintos tipos de errores cometidos al programar. Echa un vistazo aquí para GCC y aquí para LLVM, en ambos casos busca las opciones que tienen que ver con sanitize. No solo hay para detectar errores cometidos al programar con hilos sino también para otro tipo de errores habituales.
Ejemplo: Creación de hilos usando la librería
pthread
en sistemas Unix. Cada hilo ejecuta una función en paralelo y al final todos los hilos se sincronizan, esperando a que terminen./**********************************************/ /* hilos */ /* compilación: cc -o hilos hilos.c -lpthread */ /**********************************************/ #include <pthread.h> #include <stdio.h> #include <string.h> #include <stdlib.h> #define NUM_HILOS 5 int I = 0; void *codigo_del_hilo (void *id) { int i; for( i = 0; i < 50; i++) printf("Hilo %d: i = %d, I = %d\n", *(int *)id, i, I++); pthread_exit (id); } int main() { int h; pthread_t hilos[NUM_HILOS]; int id[NUM_HILOS] = {1,2,3,4,5}; int error; int *salida; for(h = 0; h < NUM_HILOS; h++) { error = pthread_create( &hilos[h], NULL, codigo_del_hilo, &id[h]); if (error){ fprintf (stderr, "Error: %d: %s\n", error, strerror (error)); exit(-1); } } for(h =0; h < NUM_HILOS; h++) { error = pthread_join(hilos[h], (void **)&salida); if (error) fprintf (stderr, "Error: %d: %s\n", error, strerror (error)); else printf ("Hilo %d terminado\n", *salida); } }
Hilos en Python
- Python permite crear hilos usando el módulo nativo
threading
. - Un hilo es una unidad de ejecución dentro de un proceso.
- Son útiles especialmente para tareas de entrada/salida (I/O), como leer archivos o esperar respuestas de red.
- Creación: se define una función y se crea un objeto
Thread
especificando esa función. - Ejecución: los hilos se inician con
start()
y se puede esperar a que terminen conjoin()
. - Limitación: el GIL impide el paralelismo real en código Python puro, pero los hilos sirven para la concurrencia en I/O.
Ejemplo Uso básico de hilos en Python con el módulo
threading
.#! /usr/bin/env python import threading THREADS = 2 MAX_COUNT = 10000000 counter = 0 def thread(): global counter print("Thread {}".format(threading.current_thread().name)) for i in range(MAX_COUNT//THREADS): counter += 1 def main(): threads = [] for i in range(THREADS): # Create new threads t = threading.Thread(target=thread) threads.append(t) t.start() # start the thread # Wait for all threads to complete for t in threads: t.join() print("Counter value: {} Expected: {}\n".format(counter, MAX_COUNT)) if __name__ == "__main__": main()
Hilos en Windows
-
En Windows, los hilos se crean utilizando la función
CreateThread
de la API del sistema. -
Un hilo es una unidad de ejecución que corre en paralelo dentro del mismo proceso.
-
Se define una función que ejecutará el hilo.
-
Con
CreateThread
, se lanza el hilo, pasando como parámetro la función a ejecutar. -
Para sincronizar, se puede usar
WaitForSingleObject
para esperar a que el hilo termine. -
Esta API permite mayor control sobre la ejecución y gestión de hilos en aplicaciones Windows nativas.
-
La función principal de creación de hilos en el
API
de Windows es CreateThread.
#include <windows.h>
#include <iostream>
using namespace std;
DWORD Cont=0; // Variable compartida
DWORD WINAPI incrementar(LPVOID param)
{
DWORD n = *(DWORD*)param;
//int i;
for(int i = 0; i < n; i++)
{
Cont++;
cout << "Contador sumando = " << Cont << "\n";
}
return 0;
}
DWORD WINAPI decrementar(LPVOID param)
{
DWORD n = *(DWORD*)param;
for (i = 0; i < n; i++)
{
Cont--;
cout << "Contador restando = " << Cont << "\n";
}
return 0;
}
int main(int argc, char *argv[]) {
DWORD TIdi,TIdd;
HANDLE THandlei,THandled;
int param = 100;
//Creamos dos threads
THandlei = CreateThread(NULL,0,incrementar,¶m,0,&TIdi);
THandled = CreateThread(NULL,0,decrementar,¶m,0,&TIdd);
cout << "Contador = " << Cont << "\n";
//Esperamos a que acaben todos los threads
WaitForSingleObject(THandlei,INFINITE);
WaitForSingleObject(THandled,INFINITE);
//Eliminamos los threads
CloseHandle(THandlei);
CloseHandle(THandled);
cout << "Contador = " << Cont << "\n";
system("PAUSE");
}
Hilos en Java
Hilos en Java: ciclo de vida
Figure 4: Ciclo de vida en Java.
Hilos y objetos
- Los hilos se representan en Java mediante la clase Thread.
- Sus métodos junto con algunos de la clase Object nos permiten un manejo completo de los hilos.
- Para cada programa Java existe un hilo de ejecución denominado hilo principal.
- Diferencia entre objeto e hilo:
- Un objeto es algo estático, con una serie de atributos y métodos.
- Pero quien ejecuta esos métodos es el hilo de ejecución.
Creación de hilos
- Clase
Thread
de Java - Dos posibilidades:
- Heredar de la clase
Thread
public class Filosofo extends Thread { ... public void run() { ... } ... }
- Implementar la interfaz
Runnable
e instanciar unThread
con un objeto que implemente esta interfaz.public class Filosofo implements Runnable { ... public void run() { ... } ... }
- Heredar de la clase
- En ambos casos hay que definir el método
run()
. Este método:- Contiene el código del hilo.
- Se invoca cuando se ejecuta el hilo.
- Cuando acaba de ejecutarse el método, el hilo termina.
- Para ejecutar un hilo hay que instanciar un objeto
Thread
y llamar al métodostart()
://creación Thread filosofo = new Filosofo(); //herencia Thread filosofo = new Thread(new Filosofo()); //interface //ejecución para ambos casos filosofo.start();
Heredando de Thread
-
Heredando de Thread y redefiniendo el método
run
class ThreadConHerencia extends Thread { String palabra; public ThreadConHerencia (String p) { palabra=p; } public void run() { for (int i = 0; i < 10; i++) { System.out.println(palabra); } } public static void main(String[]args){ Thread a = new ThreadConHerencia("hilo1"); Thread b = new ThreadConHerencia("hilo2"); a.start(); b.start(); System.out.println("Fin del hilo principal"); } }
-
Se intercalan las salidas de los tres hilos, recordemos que tenemos el hilo principal y los dos creados
Implementando la interfaz Runnable
-
Implementamos la interfaz
Runnable
, esta interfaz sólo tiene un método con la signaturapublic void run()
. -
Este método es el que como mínimo tenemos que implementar en la clase.
public class ThreadConRunnable implements Runnable { String palabra; public ThreadConRunnable (String p){ palabra=p; } public void run() { for(int i = 0; i < 10; i++) System.out.print(palabra); } }
-
Hasta aquí simplemente hemos creado una clase. Al contrario que antes, los objetos de esta clase no serán hilos ya que no hemos heredado de Thread.
-
Si queremos que el objeto de esta clase se ejecute como un hilo independiente debemos crear un objeto de la clase Thread y pasarle como parámetro el objeto donde queremos que empiece su ejecución ese hilo.
public static void main(String[]args){ ThreadConRunnable a = new ThreadConRunnable("hilo1"); ThreadConRunnable b = new ThreadConRunnable("hilo2"); Thread t1 = new Thread(a); Thread t2 = new Thread(b); t1.start(); t2.start(); System.out.println("Fin del hilo principal"); }
-
Se invoca al método
start
de la claseThread
que será el que se encarga de invocar al métodorun()
de los objetosa
yb
respectivamente -
Si comparamos ambos métodos, la segunda forma puede parecer más confusa.
-
Sin embargo es más apropiada debido a que en Java no hay herencia múltiple, al utilizar la primera opción nuestra clase ya no podría heredar de otras clases.
-
Si necesitamos que haya herencia de otras clases deberemos usar siempre la segunda opción.
Objetos autónomos en hilos
-
A veces necesitamos que un objeto autónomo se ejecute automáticamente en un nuevo hilo, sin intervención del cliente:
public class ObtejoAutonomo implements Runnable { private Thread hilo; public ObjetoAutónomo() { hilo = new Thread(this); hilo.start(); } public void run() { if (hilo == Thread.currentThread()){ //Hacer algo } } //ATENCIÓN public static void main(String []args){ ObjetoAutónomo objeto = new ObjetoAutónomo(); } }
-
Como vemos en este ejemplo, en la implementación del método run() hay que controlar cuál es el hilo que se está ejecutando y para ello nos servimos del método
currentThread()
de la claseThread
. -
Este método nos devuelve una referencia al hilo que está ejecutando ese código. Esto se hace para evitar que cualquier método de un hilo distinto haga una llamada a
run()
directamente. -
Facilita el diseño de clases cuyo comportamiento requiere ejecución concurrente, haciendo más transparente y modular el uso de hilos para quien utiliza la clase.
Estado y propiedades de los hilos
- Método
isAlive()
para saber si un hilo está vivo o muerto - Sistema de prioridades para el scheduler de la JVM:
setPriority(prioridad)
- Método
yield()
para forzar la salida de un hilo de la CPU - Otros métodos de utilidad:
wait()
,notify
,sleep(milisegundos)
Planificación y prioridades
-
Las prioridades de cada hilo en Java van de 1 (
MIN_PRIORITY
) a 10 (MAX_PRIORITY
). -
La prioridad de un hilo inicialmente es la misma que la del hilo que lo creó.
-
Por defecto, todo hilo tiene prioridad 5 (
NORM_PRIORITY
) -
La especificación de la máquina virtual no fuerza al uso de ningun algoritmo particular en la planificación de hilos.
-
El planificador debe dar ventaja a los hilos con mayor prioridad.
-
Si hay varios hilos con igual prioridad todos se deben ejecutar en algún momento.
-
No se garantiza que hilos de prioridad baja pasen a ejecutarse si existe algún hilo de mayor prioridad…, pero podría ser así.
-
El código siguiente permite comprobar la implementación particular de nuestra máquina virtual
public class ComprobarPrioridad implements Runnable { int num; ComprobarPrioridad(int c) { num = c; } public void run() { while (true) System.out.println(num); } public static void main(String[] args) { Thread nueva; for (int c = 0; c < 10; c++) { nueva = new Thread(new ComprobarPrioridad(c)); if (c == 0) nueva.setPriority(Thread.MAX_PRIORITY); nueva.start(); } } } // class
La clase Thread
-
Atributos:
public static final int MIN_PRIORITY
public static final int NORM_PRIORITY
public static final int MAX_PRIORITY
-
Constructores:
public Thread()
: por defectopublic Thread(String name)
: un nuevo hilo con nombre namepublic Thread(Runnable target)
: crea un nuevo hilo siendo target el que contiene el método run() que será invocado al lanzar el hilo con start()public Thread(Runnable target, String name)
: como el anterior, pero con nombre
-
Métodos:
public static Thread currentThread()
: retorna la referencia al hilo que se está ejecutando actualmentepublic String getName()
: retorna el nombre del hilo.int getPriority()
: retorna la prioridad del hilopublic final boolean isAlive()
: chequea si el hilo está vivopublic void run()
: contiene lo que el hilo debe hacerpublic final void setName(String name)
: cambia el nombre del hilo por namepublic final void setPriority(int nuevaPrioridad)
: cambia la prioridadpublic static void sleep(long milis)
: cesa la ejecución milis milisengudospublic void start()
: hace que el hilo comience la ejecuciónpublic static void yield ()
: hace que el hilo que se está ejecutando actualmente pase a estado de listo, permitiendo a otro hilo ganar el procesadorpublic final void join()
: Espera a que el hilo termine.
Atomicidad en Java
- Las asignaciones entre tipos primitivos son atómicas. En el caso de
long
ydouble
puede haber excepciones. - Las asignaciones de referencias son atómicas.
- Las asignaciones de variables
volatile
son atómicas. - Todas las operaciones de las clases de
java.concurrent.Atomic*
son atómicas.
Es conveniento que consultes la especificación de la versión de Java que uses.
Hilos en Rust
Creación de hilos.
-
Todo lo relacionado con hilos se encuentra en el módulo:
std::thread
. -
Creamos un hilo mediante la llamada a la función
std::thread::spawn
.use std::thread; use std::time::Duration; fn main() { thread::spawn(|| { for i in 1..10 { println!("hi number {} from the spawned thread!", i); thread::sleep(Duration::from_millis(1)); } }); for i in 1..5 { println!("hi number {} from the main thread!", i); thread::sleep(Duration::from_millis(1)); } }
Compartir información entre hilos: Mutex + Arc (Atomic Reference Counter).
-
Hacemos use de Mutex.
use std::sync::Mutex; use std::thread; fn main() { let counter = Mutex::new(0); let mut handles = vec![]; for _ in 0..10 { let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!("Result: {}", *counter.lock().unwrap()); }
-
Este código no compila. ¿Por qué no lo hace?
error[E0382]: borrow of moved value: `counter`
-
Esta versión si lo hace. Fíjate que usamos Arc y no Rc. La diferencia está en los traits Send y Sync.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
Otros tipos de datos útiles al usar hilos en rust.
- En determinadas ocasiones al trabajar con hilos te puede resultar útil el patron llamado mutabilidad interior.
- Este patrón se implementa por tipos como: std::cell::RefCell.
Aclaraciones
- En ningún caso estas transparencias son la bibliografía de la asignatura, por lo tanto debes estudiar, aclarar y ampliar los conceptos que en ellas encuentres empleando los enlaces web y bibliografía recomendada que puedes consultar en la página web de la ficha de la asignatura y en la web propia de la asignatura.