multithreadingEmpezando con multihilo


Observaciones

El subprocesamiento múltiple es una técnica de programación que consiste en dividir una tarea en subprocesos separados de ejecución. Estos subprocesos se ejecutan de forma simultánea, ya sea mediante la asignación a diferentes núcleos de procesamiento o mediante el corte de tiempo.

Al diseñar un programa de multiproceso, los hilos deben hacerse lo más independientes posible entre sí, para lograr la mayor velocidad.
En la práctica, los hilos rara vez son totalmente independientes, lo que hace que la sincronización sea necesaria.
La máxima aceleración teórica se puede calcular utilizando la ley de Amdahl .

Ventajas

  • Acelere el tiempo de ejecución utilizando los recursos de procesamiento disponibles de manera eficiente
  • Permita que un proceso siga respondiendo sin la necesidad de dividir cálculos largos o operaciones de E / S costosas
  • Priorizar fácilmente ciertas operaciones sobre otras

Desventajas

  • Sin un diseño cuidadoso, se pueden introducir errores difíciles de encontrar
  • Crear hilos implica algo de sobrecarga

¿Puede el mismo hilo correr dos veces?

La pregunta más frecuente es que un mismo hilo puede ejecutarse dos veces.

La respuesta para esto es saber que un hilo solo puede ejecutarse una vez.

si intenta ejecutar el mismo hilo dos veces, se ejecutará por primera vez, pero dará un error por segunda vez y el error será IllegalThreadStateException.

ejemplo :

public class TestThreadTwice1 extends Thread{  
 public void run(){  
   System.out.println("running...");  
 }  
 public static void main(String args[]){  
  TestThreadTwice1 t1=new TestThreadTwice1();  
  t1.start();  
  t1.start();  
 }  
}  
 

salida :

running
       Exception in thread "main" java.lang.IllegalThreadStateException
 

Puntos muertos

Se produce un punto muerto cuando cada miembro de un grupo de dos o más subprocesos debe esperar a que uno de los otros miembros haga algo (por ejemplo, para liberar un bloqueo) antes de que pueda continuar. Sin intervención, los hilos esperarán por siempre.

Un ejemplo de pseudocódigo de un diseño propenso a interbloqueo es:

thread_1 {
    acquire(A)
    ...
    acquire(B)
    ...
    release(A, B)
}

thread_2 {
    acquire(B)
    ...
    acquire(A)
    ...
    release(A, B)
}
 

Se puede producir un punto muerto cuando el thread_1 ha adquirido A , pero aún no B , y thread_2 ha adquirido B , pero no A Como se muestra en el siguiente diagrama, ambos hilos esperarán por siempre. Diagrama de interbloqueo

Cómo evitar los puntos muertos

Como regla general, minimice el uso de bloqueos y minimice el código entre el bloqueo y el desbloqueo.

Adquirir cerraduras en el mismo orden

Un rediseño de thread_2 resuelve el problema:

thread_2 {
    acquire(A)
    ...
    acquire(B)
    ...
    release(A, B)
}
 

Ambos hilos adquieren los recursos en el mismo orden, evitando así los interbloqueos.

Esta solución se conoce como la "solución de jerarquía de recursos". Fue propuesto por Dijkstra como una solución al "problema de los filósofos que comen".

A veces, incluso si especifica un orden estricto para la adquisición de bloqueos, dicho orden de adquisición de bloqueo estático se puede dinamizar en el tiempo de ejecución.

Considere el siguiente código:

void doCriticalTask(Object A, Object B){
     acquire(A){
        acquire(B){
            
        }
    }
}
 

Aquí incluso si la orden de adquisición de bloqueo parece segura, puede causar un interbloqueo cuando thread_1 accede a este método con, digamos, Object_1 como parámetro A y Object_2 como parámetro B y thread_2 lo hace en orden opuesto, es decir, Object_2 como parámetro A y Object_1 como parámetro B.

En tal situación, es mejor tener alguna condición única derivada utilizando tanto Object_1 como Object_2 con algún tipo de cálculo, por ejemplo, utilizando el código hash de ambos objetos, por lo que cada vez que un hilo diferente ingrese en ese método en cualquier orden paramétrico, cada vez que esa condición única se derive bloqueo de orden de adquisición.

por ejemplo, Say Object tiene una clave única, por ejemplo, accountNumber en el caso del objeto Account.

void doCriticalTask(Object A, Object B){
    int uniqueA = A.getAccntNumber();
    int uniqueB = B.getAccntNumber();
    if(uniqueA > uniqueB){
         acquire(B){
            acquire(A){
                
            }
        }
    }else {
         acquire(A){
            acquire(B){
                
            }
        }
    }
}
 

Hello Multithreading - Creando nuevos hilos.

Este sencillo ejemplo muestra cómo iniciar múltiples hilos en Java. Tenga en cuenta que no se garantiza que los subprocesos se ejecuten en orden, y el orden de ejecución puede variar para cada ejecución.

public class HelloMultithreading {

    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(new MyRunnable(i));
            t.start();
        }
    }

    public static class MyRunnable implements Runnable {

        private int mThreadId;

        public MyRunnable(int pThreadId) {
            super();
            mThreadId = pThreadId;
        }

        @Override
        public void run() {
            System.out.println("Hello multithreading: thread " + mThreadId);
        }

    }

}
 

Propósito

Los hilos son las partes de bajo nivel de un sistema informático en el que se produce el procesamiento de comandos. Es compatible / proporcionado por el hardware de CPU / MCU. También hay métodos de software. El propósito de los subprocesos múltiples es hacer cálculos en paralelo entre sí, si es posible. Por lo tanto, el resultado deseado se puede obtener en una porción de tiempo más pequeña.

Condiciones de carrera

Una condición de carrera o carrera de datos es un problema que puede ocurrir cuando un programa de multiproceso no está sincronizado correctamente. Si dos o más subprocesos acceden a la misma memoria sin sincronización, y al menos uno de los accesos es una operación de "escritura", se produce una carrera de datos. Esto conduce a un comportamiento del programa dependiente, posiblemente incoherente. Por ejemplo, el resultado de un cálculo podría depender de la programación de subprocesos.

Problema de los lectores-escritores :

writer_thread {
    write_to(buffer)
}

reader_thread {
    read_from(buffer)
}
 

Una solución simple:

writer_thread {
    lock(buffer)
    write_to(buffer)
    unlock(buffer)
}

reader_thread {
    lock(buffer)
    read_from(buffer)
    unlock(buffer)
}
 

Esta solución simple funciona bien si solo hay un subproceso del lector, pero si hay más de uno, ralentiza la ejecución innecesariamente, porque los subprocesos del lector pueden leer simultáneamente.

Una solución que evita este problema podría ser:

writer_thread {
    lock(reader_count)
    if(reader_count == 0) {
        write_to(buffer)
    }
    unlock(reader_count)
}

reader_thread {
    lock(reader_count)
    reader_count = reader_count + 1
    unlock(reader_count)

    read_from(buffer)

    lock(reader_count)
    reader_count = reader_count - 1
    unlock(reader_count)
}
 

Tenga en cuenta que reader_count está bloqueado durante toda la operación de escritura, por lo que ningún lector puede comenzar a leer mientras la escritura no haya terminado.

Ahora muchos lectores pueden leer simultáneamente, pero puede surgir un nuevo problema: el reader_count nunca puede alcanzar 0 , por lo que el hilo del escritor nunca puede escribir en el búfer. Esto se llama inanición , existen diferentes soluciones para evitarlo.


Incluso los programas que pueden parecer correctos pueden ser problemáticos:

boolean_variable = false 

writer_thread {
    boolean_variable = true
}

reader_thread {
    while_not(boolean_variable)
    {
       do_something()
    }         
}
 

Es posible que el programa de ejemplo nunca finalice, ya que el subproceso del lector nunca verá la actualización del subproceso del escritor. Si, por ejemplo, el hardware utiliza cachés de CPU, es posible que los valores se almacenen en caché. Y dado que una escritura o lectura en un campo normal, no lleva a una actualización de la memoria caché, el hilo de lectura nunca verá el valor modificado.

C ++ y Java definen en el llamado modelo de memoria, lo que correctamente sincronizado significa: C ++ Memory Model , Java Memory Model .

En Java, una solución sería declarar el campo como volátil:

volatile boolean boolean_field;
 

En C ++, una solución sería declarar el campo como atómico:

std::atomic<bool> data_ready(false)
 

Una carrera de datos es un tipo de condición de carrera. Pero no todas las condiciones de carrera son carreras de datos. Lo siguiente llamado por más de un hilo conduce a una condición de carrera pero no a una carrera de datos:

class Counter {
    private volatile int count = 0;

    public void addOne() {
     i++;
    }
}
 

Se sincroniza correctamente de acuerdo con la especificación del modelo de memoria Java, por lo que no es una carrera de datos. Pero todavía conduce a condiciones de carrera, por ejemplo, el resultado depende de la intercalación de los hilos.

No todas las razas de datos son errores. Un ejemplo de una condición llamada raza benigna es sun.reflect.NativeMethodAccessorImpl:

class  NativeMethodAccessorImpl extends MethodAccessorImpl {
    private Method method;
    private DelegatingMethodAccessorImpl parent;
    private int numInvocations;
    
    NativeMethodAccessorImpl(Method method) {
        this.method = method;
    }

    public Object invoke(Object obj, Object[] args)
        throws IllegalArgumentException, InvocationTargetException
    {
        if (++numInvocations > ReflectionFactory.inflationThreshold()) {
              MethodAccessorImpl acc = (MethodAccessorImpl)
            new MethodAccessorGenerator().
            generateMethod(method.getDeclaringClass(),
                             method.getName(),
                             method.getParameterTypes(),
                             method.getReturnType(),
                             method.getExceptionTypes(),
                             method.getModifiers());
                             parent.setDelegate(acc);
          }
          return invoke0(method, obj, args);
    }
    ...
}
 

Aquí el rendimiento del código es más importante que la corrección del recuento de numInvocation.