Procesos

Concepto y ciclo de vida

  • El ciclo de vida de un proceso es el siguiente:

ciclovida.png

  • 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 a fork(). Puedes ver un contraargumento al uso de fork en este artículo. Puedes ver toda la información relativa a fork 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 a fork para crear el proceso 1, 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…

monitorprocsos.png

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

hiloshw.svg

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- o TLS.

    1. Lenguajes como D lo usan por defecto.

    2. En el caso de C++ debemos emplear C++11 o superior para tener soporte TLS.

    3. En el caso de Rust disponemos de la macro thread_local!

    4. 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 con join().
  • 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,&param,0,&TIdi);
    THandled = CreateThread(NULL,0,decrementar,&param,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

ciclovidajava.png

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 un Thread con un objeto que implemente esta interfaz.
      public class Filosofo implements Runnable {
          ...
          public void run() { ... }
          ...
      }
      
  • 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étodo start():
    //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 signatura public 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 clase Thread que será el que se encarga de invocar al método run() de los objetos a y b 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 clase Thread.

  • 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 defecto
    • public Thread(String name): un nuevo hilo con nombre name
    • public 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 actualmente
    • public String getName(): retorna el nombre del hilo.
    • int getPriority(): retorna la prioridad del hilo
    • public final boolean isAlive(): chequea si el hilo está vivo
    • public void run(): contiene lo que el hilo debe hacer
    • public final void setName(String name): cambia el nombre del hilo por name
    • public final void setPriority(int nuevaPrioridad): cambia la prioridad
    • public static void sleep(long milis): cesa la ejecución milis milisengudos
    • public void start(): hace que el hilo comience la ejecución
    • public static void yield (): hace que el hilo que se está ejecutando actualmente pase a estado de listo, permitiendo a otro hilo ganar el procesador
    • public 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 y double 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.

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.