Principio de implementación ThreadPoolExecutor del grupo de subprocesos C ++

1. Por qué utilizar el grupo de subprocesos

En el uso real, los subprocesos ocupan recursos del sistema. Una mala gestión de los subprocesos puede conducir fácilmente a problemas del sistema. Por lo tanto, los grupos de subprocesos se utilizan para administrar subprocesos en la mayoría de los marcos de concurrencia. Los principales beneficios de usar grupos de subprocesos para administrar subprocesos son los siguientes:

  1. Reducir el consumo de recursos . Reduzca la pérdida de rendimiento del sistema tanto como sea posible reutilizando los subprocesos existentes y reduciendo el número de cierres de subprocesos;
  2. Mejore la velocidad de respuesta del sistema . Al reutilizar subprocesos, se omite el proceso de creación de subprocesos, mejorando así la velocidad de respuesta del sistema en su conjunto;
  3. Mejorar la capacidad de administración de los subprocesos . El subproceso es un recurso escaso, si se crea sin límite, no solo consumirá recursos del sistema, sino que también reducirá la estabilidad del sistema, por lo que es necesario utilizar un grupo de subprocesos para administrar los subprocesos.

2. Cómo funciona el grupo de subprocesos

Cuando se envía una tarea simultánea al grupo de subprocesos, el grupo de subprocesos asigna subprocesos para ejecutar la tarea como se muestra en la siguiente figura:

 

Diagrama de flujo de ejecución del grupo de subprocesos.jpg

Como se puede ver en la figura, el grupo de subprocesos ejecuta las tareas enviadas en las siguientes etapas:

  1. Primero, determine si todos los subprocesos del grupo de subprocesos principales del grupo de subprocesos están realizando tareas. De lo contrario, cree un nuevo hilo para ejecutar la tarea que acaba de enviar; de lo contrario, todos los hilos del grupo de hilos principales están ejecutando tareas, luego vaya al paso 2;
  2. Determine si la cola de bloqueo actual está llena, si no, coloque la tarea enviada en la cola de bloqueo; de lo contrario, vaya al paso 3;
  3. Determine si todos los subprocesos del grupo de subprocesos están realizando tareas; de lo contrario, cree un nuevo subproceso para realizar tareas; de lo contrario, entréguelo a la estrategia de saturación para su procesamiento.

3. Implementación del grupo de subprocesos

Los amigos que no entiendan pueden ver esto, una explicación en video de la realidad del grupo de subprocesos, haga clic en: 150 líneas de código, grupo de subprocesos escrito a mano

4. Creación de grupos de subprocesos

La creación del grupo de subprocesos se realiza principalmente mediante la clase ThreadPoolExecutor . ThreadPoolExecutor tiene muchos métodos de construcción sobrecargados. El método de construcción con la mayoría de los parámetros se utiliza para comprender los parámetros que deben configurarse para crear el grupo de subprocesos. El método de construcción de ThreadPoolExecutor es:

ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

Los parámetros se describen a continuación:

  1. corePoolSize: indica el tamaño del grupo de subprocesos principales. Al enviar una tarea, si el número de subprocesos en el grupo de subprocesos del núcleo actual no alcanza corePoolSize, se creará un nuevo subproceso para ejecutar la tarea enviada, incluso si hay subprocesos inactivos en el grupo de subprocesos del núcleo actual . Si el número de subprocesos en el grupo de subprocesos del núcleo actual ha alcanzado corePoolSize, no se volverán a crear más subprocesos. Si se llama a prestartCoreThread () o prestartAllCoreThreads (), todos los subprocesos principales se crearán e iniciarán cuando se cree el grupo de subprocesos.
  2. maximumPoolSize: indica el número máximo de subprocesos que puede crear el grupo de subprocesos. Si cuando la cola de bloqueo está llena y el número de subprocesos en el grupo de subprocesos actual no excede el maximumPoolSize, se creará un nuevo subproceso para realizar la tarea.
  3. keepAliveTime: tiempo de supervivencia del hilo inactivo. Si el número de subprocesos en el grupo de subprocesos actual ha excedido corePoolSize, y el tiempo de inactividad del subproceso excede keepAliveTime, estos subprocesos inactivos se destruirán, lo que puede reducir el consumo de recursos del sistema tanto como sea posible.
  4. unidad: unidad de tiempo. Especifique la unidad de tiempo para keepAliveTime.
  5. workQueue: cola de bloqueo. La cola de bloqueo utilizada para guardar tareas, puede leer este artículo sobre el bloqueo de colas. Puede utilizar ArrayBlockingQueue, LinkedBlockingQueue, SynchronousQueue, PriorityBlockingQueue .
  6. threadFactory: la clase de ingeniería para crear subprocesos. Puede establecer un nombre más significativo para cada hilo creado especificando la fábrica de hilos Si hay un problema de concurrencia, también es conveniente encontrar la causa del problema.
  7. handler: Estrategia de saturación. Cuando la cola de bloqueo del grupo de subprocesos está llena y los subprocesos especificados se han abierto, lo que indica que el grupo de subprocesos actual ya está en un estado saturado, se necesita una estrategia para hacer frente a esta situación. Se han adoptado varias estrategias:
    1. AbortPolicy: rechaza directamente la tarea enviada y lanza una excepción RejectedExecutionException ;
    2. CallerRunsPolicy: solo usa el hilo de la persona que llama para realizar tareas;
    3. DiscardPolicy: descarte la tarea directamente sin procesarla;
    4. DiscardOldestPolicy: descarte la tarea almacenada más larga en la cola de bloqueo y ejecute la tarea actual

Comparta más sobre C / C ++ Desarrollo de back-end de Linux de los principios subyacentes del conocimiento y la red de aprendizaje para mejorar los materiales de aprendizaje , la pila de tecnología completa, el conocimiento del contenido, incluidos Linux, Nginx, ZeroMQ, MySQL, Redis, grupo de subprocesos, MongoDB , ZK, transmisión de medios, audio y video, kernel de Linux, CDN, P2P, epoll, Docker, TCP / IP, coroutine, DPDK, etc.

Lógica de ejecución del grupo de subprocesos

Después de que se crea el grupo de subprocesos a través de ThreadPoolExecutor, el proceso de ejecución después de que se envía la tarea, echemos un vistazo al código fuente. El código fuente del método de ejecución es el siguiente:

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    /*
     * Proceed in 3 steps:
     *
     * 1. If fewer than corePoolSize threads are running, try to
     * start a new thread with the given command as its first
     * task.  The call to addWorker atomically checks runState and
     * workerCount, and so prevents false alarms that would add
     * threads when it shouldn't, by returning false.
     *
     * 2. If a task can be successfully queued, then we still need
     * to double-check whether we should have added a thread
     * (because existing ones died since last checking) or that
     * the pool shut down since entry into this method. So we
     * recheck state and if necessary roll back the enqueuing if
     * stopped, or start a new thread if there are none.
     *
     * 3. If we cannot queue task, then we try to add a new
     * thread.  If it fails, we know we are shut down or saturated
     * and so reject the task.
     */
    int c = ctl.get();
	//如果线程池的线程个数少于corePoolSize则创建新线程执行当前任务
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
	//如果线程个数大于corePoolSize或者创建线程失败,则将任务存放在阻塞队列workQueue中
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
	//如果当前任务无法放进阻塞队列中,则创建新的线程来执行任务
    else if (!addWorker(command, false))
        reject(command);
}

Consulte las notas para conocer la lógica de ejecución del método de ejecución de ThreadPoolExecutor. La siguiente figura muestra el diagrama de ejecución del método de ejecución de ThreadPoolExecutor:

 

Diagrama esquemático del proceso de ejecución de ejecución.jpg

Hay varias situaciones en la lógica de ejecución del método de ejecución:

  1. Si los subprocesos que se están ejecutando actualmente son menores que corePoolSize, se crearán nuevos subprocesos para realizar nuevas tareas;
  2. Si el número de subprocesos en ejecución es igual o mayor que corePoolSize, las tareas enviadas se almacenarán en la cola de bloqueo workQueue;
  3. Si la cola workQueue actual está llena, se creará un nuevo hilo para realizar la tarea;
  4. Si el número de subprocesos ha superado el maximumPoolSize, se utilizará la estrategia de saturación RejectedExecutionHandler para el procesamiento.

Cabe señalar que la idea de diseño del grupo de subprocesos es usar el grupo de subprocesos central corePoolSize, la cola de bloqueo workQueue y el grupo de subprocesos maximumPoolSize , como una estrategia de almacenamiento en caché para procesar tareas, de hecho, esta idea de diseño se utilizará en el marco.

5. Cerrar el grupo de subprocesos

Para cerrar el grupo de subprocesos, puede utilizar los métodos shutdown y shutdownNow. Su principio es atravesar todos los subprocesos en el grupo de subprocesos y luego interrumpir los subprocesos a su vez. Todavía existen diferencias entre el apagado y el apagado Ahora:

  1. shutdownNow primero establece el estado del grupo de subprocesos en DETENER , luego intenta detener todos los subprocesos que están ejecutando y no ejecutando tareas , y devuelve la lista de tareas en espera de ser ejecutadas;
  2. Shutdown simplemente establece el estado del grupo de subprocesos en el estado de APAGADO y luego interrumpe todos los subprocesos que no están realizando tareas

Se puede ver que el método de apagado continuará la ejecución de la tarea que se está ejecutando, y shutdownNow interrumpirá directamente la tarea que se está ejecutando. Cuando se llama a cualquiera de estos dos métodos, el método isShutdown devolverá verdadero. Cuando todos los subprocesos se cierran correctamente, significa que el grupo de subprocesos se ha cerrado correctamente. En este momento, el método isTerminated devolverá verdadero.

5. ¿Cómo configurar razonablemente los parámetros del grupo de subprocesos?

Si desea configurar el grupo de subprocesos de manera razonable, primero debe analizar las características de la tarea, que se pueden analizar desde las siguientes perspectivas:

  1. La naturaleza de la tarea: tareas intensivas en CPU, tareas intensivas en IO y tareas mixtas.
  2. La prioridad de la tarea: alta, media y baja.
  3. Tiempo de ejecución de la tarea: largo, medio y corto.
  4. Dependencia de tareas: si depende de otros recursos del sistema, como las conexiones de la base de datos.

Las tareas con diferente naturaleza de tareas se pueden procesar por separado mediante grupos de subprocesos de diferentes tamaños. Configure las tareas de uso intensivo de la CPU con la menor cantidad de subprocesos posible, como configurar un grupo de subprocesos con subprocesos Ncpu + 1 . Las tareas con uso intensivo de E / S deben esperar a que se realicen operaciones de E / S, y los subprocesos no siempre realizan tareas, por lo que debe configurar tantos subprocesos como sea posible, como 2xNcpu . Para las tareas mixtas, si se pueden dividir, divídalas en una tarea con uso intensivo de CPU y una tarea intensiva en E / S. Siempre que la diferencia de tiempo entre la ejecución de las dos tareas no sea demasiado grande, la tasa de rendimiento después de la descomposición será Debido a la tasa de rendimiento de la ejecución en serie, si el tiempo de ejecución de estas dos tareas difiere demasiado, no hay necesidad de descomponer. Podemos obtener el número de CPU del dispositivo actual a través del método Runtime.getRuntime (). AvailableProcessors ().

Las tareas con diferentes prioridades se pueden procesar utilizando la cola de prioridad PriorityBlockingQueue. Permite que las tareas de alta prioridad se ejecuten primero. Debe tenerse en cuenta que si siempre hay tareas de alta prioridad enviadas a la cola, es posible que las tareas de baja prioridad nunca se ejecuten.

Las tareas con diferentes tiempos de ejecución se pueden transferir a grupos de subprocesos de diferentes tamaños para su procesamiento, o se pueden usar colas de prioridad para permitir que las tareas con tiempos de ejecución cortos se ejecuten primero.

Confíe en la tarea del grupo de conexiones de la base de datos, porque el subproceso debe esperar a que la base de datos devuelva el resultado después de enviar el SQL. Cuanto más largo sea el tiempo de espera, mayor será el tiempo de inactividad de la CPU y mayor será el número de subprocesos set, para que la CPU se pueda utilizar mejor.

Además, la cola de bloqueo es mejor usar una cola limitada . Si se usa una cola ilimitada, una vez que la tarea pendiente está en la cola de bloqueo, consumirá demasiados recursos de memoria e incluso hará que el sistema se bloquee.

Supongo que te gusta

Origin blog.csdn.net/Linuxhus/article/details/115030132
Recomendado
Clasificación