Diferentes enfoques de la programación concurrente en Java

Este artículo muestra cómo realizar la programación concurrente utilizando el marco de subprocesamiento de Java. Analicemos primero la programación concurrente:

Programación concurrente: esto significa que las tareas parecen ejecutarse simultáneamente, pero bajo el capó, el sistema realmente podría estar cambiando de una tarea a otra. El punto de la programación concurrente es que es beneficiosa incluso en una máquina con un solo procesador.

Programación simultánea en una máquina con un solo procesador:

  1. Suponga que el usuario necesita descargar cinco imágenes y cada imagen proviene de un servidor diferente, y cada imagen tarda cinco segundos, y ahora suponga que el usuario descarga todas las primeras imágenes, tarda 5 segundos, luego todas las segundas imágenes, se toma otros 5 segundos, y así sucesivamente, y al final del tiempo, tomó 25 segundos. Es más rápido descargar un poco de la imagen uno, luego un poco de la imagen dos, tres, cuatro, cinco y luego regresar y un poco de la imagen uno y así sucesivamente.
             Tasks overlap in time
    
    Task1   ------   ------   ------    ------ 
    
    Task2       ------   ------   ------   ------ 
    
            ------------------------------------------>
                                 Time
    
  2. Si toma 5 segundos para cada uno y lo divide en pequeños trozos, la suma total sigue siendo 25 segundos. Entonces, ¿por qué es más rápido descargarlo al mismo tiempo?
  3. Es porque cuando se llama a la imagen del primer servidor y tarda 5 segundos, no porque el ancho de banda entrante esté al máximo, sino porque el servidor tarda un tiempo en enviarla al usuario. Básicamente, el usuario se sienta a esperar la mayor parte del tiempo. Entonces, mientras el usuario espera la primera imagen, bien podría estar comenzando a descargar la segunda imagen. Entonces, si el servidor es lento, al hacerlo en varios subprocesos al mismo tiempo, se pueden descargar imágenes adicionales sin mucho tiempo extra.
  4. Ahora, eventualmente, si uno descarga muchas imágenes al mismo tiempo, el ancho de banda entrante puede llegar al máximo y luego agregar más subprocesos no lo acelerará, pero hasta cierto punto, es un poco gratis.
  5. Además de la velocidad, otra ventaja es la disminución de la latencia. Hacer un poco a la vez disminuye la latencia, por lo que el usuario puede ver algunos comentarios a medida que avanzan las cosas.

Necesidad de programación concurrente

  • Los subprocesos son útiles solo cuando la tarea es relativamente grande y bastante autónoma. Cuando el usuario necesita realizar solo una pequeña cantidad de combinación después de una gran cantidad de procesamiento por separado, hay algunos gastos generales para iniciar y usar subprocesos. Entonces, si la tarea es realmente pequeña, nunca se le devuelven los gastos generales.
  • Además, como se mencionó anteriormente, los hilos son más útiles cuando los usuarios están esperando. Por ejemplo, mientras uno espera un servidor, el otro puede estar leyendo desde otro servidor.

Pasos básicos para la programación concurrente

  1. En primer lugar para poner en cola una tarea. El servicio ejecutor de llamadas puntúa un nuevo grupo de subprocesos fijos y proporciona un tamaño. Este tamaño indica el número máximo de tareas simultáneas. Por ejemplo, si uno agrega mil cosas a la cola pero el tamaño del grupo es 50, entonces solo 50 de ellas se ejecutarán a la vez. Solo cuando uno de los primeros cincuenta termine de ejecutarse, se tomará el 51 para su ejecución. Un número como 100 como tamaño del grupo no sobrecargará el sistema.
    ExecutorService taskList = Executors.newFixedThreadPool(poolSize);
    
  2. Luego, el usuario tiene que poner algunas tareas de un tipo ejecutable en la cola de tareas. Runnable es solo una interfaz única que tiene un método llamado run. El sistema llama al método de ejecución en el momento apropiado cuando cambia de una tarea a otra iniciando un subproceso separado.
     taskList.execute(someRunnable)
  3. El método de ejecución es un nombre un poco inapropiado porque cuando se agrega una tarea a la tarea en la cola que se crea arriba con ejecutores punto nuevo grupo de subprocesos fijos, no necesariamente comienza a ejecutarse de inmediato. Comienza a ejecutarse cuando uno de los que se ejecutan simultáneamente (tamaño del grupo) finaliza la ejecución.

Hay cinco enfoques diferentes para implementar la programación concurrente con diferentes ventajas y desventajas. Discutiremos el primer enfoque en este artículo y los enfoques restantes en los artículos posteriores.

Enfoque uno: clase separada que implementa Runnable

  1. Lo primero que debe hacer es crear una clase separada, y una clase completamente separada, que implemente la interfaz ejecutable.
    public class MyRunnable implements Runnable {
             public void run() { ... }  
    }
  2. En segundo lugar, haga algunas instancias de la clase principal y páselas a ejecutar. Apliquemos este primer enfoque para crear hilos que solo cuenten. Entonces, cada subproceso imprimirá el nombre del subproceso, el número de tarea y el valor del contador.
  3. Después de esto, use el método de pausa para sentarse a esperar para que el sistema cambie de un lado a otro. Por lo tanto, las declaraciones de impresión se intercalarán.
  4. Pase los argumentos del constructor al constructor de Runnable, de modo que las diferentes instancias cuenten un número diferente de veces.
  5. Llamar al método de apagado significa cerrar el subproceso que está observando para ver si se agregaron nuevas tareas o no.

Implementación práctica

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
  
/**
* @author evivehealth on 08/02/19.
*/
// Java program depicting 
// concurrent programming in action.
  
// Runnable Class that defines the logic
// of run method of runnable interface
public class Counter implements Runnable 
{
    private final MainApp mainApp;
    private final int loopLimit;
    private final String task;
  
    // Constructor to get a reference to the main class
    public Counter
          (MainApp mainApp, int loopLimit, String task)
    {
        this.mainApp = mainApp;
        this.loopLimit = loopLimit;
        this.task = task;
    }
  
    // Prints the thread name, task number and 
    // the value of counter
    // Calls pause method to allow multithreading to occur
    @Override
    public void run()
    {
        for (int i = 0; i < loopLimit; i++) 
        {
            System.out.println("Thread: " +
            Thread.currentThread().getName() + " Counter: "
                             + (i + 1) + " Task: " + task);
            mainApp.pause(Math.random());
        }
    }
}
class MainApp 
{
  
    // Starts the threads. Pool size 2 means at any time
    // there can only be two simultaneous threads
    public void startThread()
    {
        ExecutorService taskList = 
                         Executors.newFixedThreadPool(2);
        for (int i = 0; i < 5; i++) 
        {
            // Makes tasks available for execution.
            // At the appropriate time, calls run 
            // method of runnable interface
            taskList.execute(new Counter(this, i + 1,
                                    "task " + (i + 1)));
        }
  
        // Shuts the thread that's watching to see if 
        // you have added new tasks.
        taskList.shutdown();
    }
  
    // Pauses execution for a moment
    // so that system switches back and forth
    public void pause(double seconds)
    {
        try 
        {
            Thread.sleep(Math.round(1000.0 * seconds));
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
  
    // Driver method
    public static void main(String[] args)
    {
        new MainApp().startThread();
    }
}
Producción:

Thread: pool-1-thread-1 Counter: 1 Task: task 1
Thread: pool-1-thread-2 Counter: 1 Task: task 2
Thread: pool-1-thread-2 Counter: 2 Task: task 2
Thread: pool-1-thread-1 Counter: 1 Task: task 3
Thread: pool-1-thread-2 Counter: 1 Task: task 4
Thread: pool-1-thread-1 Counter: 2 Task: task 3
Thread: pool-1-thread-1 Counter: 3 Task: task 3
Thread: pool-1-thread-1 Counter: 1 Task: task 5
Thread: pool-1-thread-2 Counter: 2 Task: task 4
Thread: pool-1-thread-2 Counter: 3 Task: task 4
Thread: pool-1-thread-1 Counter: 2 Task: task 5
Thread: pool-1-thread-2 Counter: 4 Task: task 4
Thread: pool-1-thread-1 Counter: 3 Task: task 5
Thread: pool-1-thread-1 Counter: 4 Task: task 5
Thread: pool-1-thread-1 Counter: 5 Task: task 5

ventajas:

  • Acoplamiento flojo: dado que se puede reutilizar una clase separada, promueve el acoplamiento flojo.
  • Constructores: los argumentos se pueden pasar a los constructores para diferentes casos. Por ejemplo, describir diferentes límites de bucle para subprocesos.
  • Condiciones de carrera: si los datos se han compartido, es poco probable que se utilice una clase separada como enfoque y si no tiene datos compartidos, entonces no hay necesidad de preocuparse por las condiciones de carrera.

Desventajas:
Fue un poco incómodo volver a llamar a la aplicación principal. Se tenía que pasar una referencia a lo largo del constructor, e incluso si hay acceso a la referencia, solo se pueden llamar los métodos públicos (método de pausa en el ejemplo dado) en la aplicación principal.

Publicación traducida automáticamente

Artículo escrito por AnureetKaur y traducido por Barcelona Geeks. The original can be accessed here. Licence: CCBY-SA

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *