Princípio de implementação ThreadPoolExecutor do pool de threads do C ++

1. Por que usar pool de threads

No uso real, os encadeamentos ocupam recursos do sistema.Uma má gestão dos encadeamentos pode facilmente levar a problemas no sistema. Portanto, pools de threads são usados para gerenciar threads na maioria das estruturas de simultaneidade. Os principais benefícios de usar pools de threads para gerenciar threads são os seguintes:

  1. Reduza o consumo de recursos . Reduza a perda de desempenho do sistema tanto quanto possível reutilizando threads existentes e reduzindo o número de desligamentos de threads;
  2. Melhore a velocidade de resposta do sistema . Ao reutilizar threads, o processo de criação de threads é omitido, melhorando assim a velocidade de resposta do sistema como um todo;
  3. Melhore a capacidade de gerenciamento de threads . Thread é um recurso escasso. Se criado sem limite, não só consumirá recursos do sistema, mas também reduzirá a estabilidade do sistema. Portanto, é necessário usar um pool de threads para gerenciar os threads.

2. Como funciona o pool de threads

Quando uma tarefa simultânea é enviada ao pool de threads, o pool de threads aloca threads para executar a tarefa, conforme mostrado na figura a seguir:

 

Fluxograma de execução do pool de threads.jpg

Como pode ser visto na figura, o pool de threads executa as tarefas enviadas nos seguintes estágios:

  1. Primeiro, determine se todos os encadeamentos no conjunto de encadeamentos principal no conjunto de encadeamentos estão executando tarefas. Caso contrário, crie um novo encadeamento para executar a tarefa que acabou de ser enviada, caso contrário, todos os encadeamentos no pool de encadeamentos principais estão executando tarefas e vá para a etapa 2;
  2. Determine se a fila de bloqueio atual está cheia, caso contrário, coloque a tarefa enviada na fila de bloqueio, caso contrário, vá para a etapa 3;
  3. Determine se todos os encadeamentos no pool de encadeamentos estão realizando tarefas, caso contrário, crie um novo encadeamento para realizar tarefas, caso contrário, passe para a estratégia de saturação para processamento

3. Implementação de pool de threads

Amigos que não entendem podem dar uma olhada nisso, uma explicação em vídeo da realidade do pool de threads, clique em: 150 linhas de código, pool de threads manuscritas

4. Criação de pool de threads

A criação do pool de threads é feita principalmente pela classe ThreadPoolExecutor . ThreadPoolExecutor tem muitos métodos de construção sobrecarregados. O método de construção com a maioria dos parâmetros é usado para entender os parâmetros que precisam ser configurados para criar o pool de threads. O método de construção de ThreadPoolExecutor é:

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

Os parâmetros são descritos a seguir:

  1. corePoolSize: indica o tamanho do pool de thread principal. Ao enviar uma tarefa, se o número de threads no pool de thread principal atual não atingir corePoolSize, um novo thread será criado para executar a tarefa enviada, mesmo se houver threads inativos no pool de thread principal atual . Se o número de encadeamentos no pool de encadeamentos principal atual atingiu corePoolSize, nenhum outro encadeamento será recriado. Se prestartCoreThread () ou prestartAllCoreThreads () for chamado, todos os threads principais serão criados e iniciados quando o pool de threads for criado.
  2. maximumPoolSize: indica o número máximo de threads que o pool de threads pode criar. Se quando a fila de bloqueio estiver cheia e o número de threads no pool de threads atual não exceder o maximumPoolSize, uma nova thread será criada para executar a tarefa.
  3. keepAliveTime: tempo de sobrevivência do thread inativo. Se o número de threads no pool de threads atual tiver excedido corePoolSize e o tempo ocioso do thread exceder keepAliveTime, esses threads inativos serão destruídos, o que pode reduzir o consumo de recursos do sistema tanto quanto possível.
  4. unidade: Unidade de tempo. Especifique a unidade de tempo para keepAliveTime.
  5. workQueue: bloqueio de fila. A fila de bloqueio usada para salvar tarefas, você pode ler este artigo sobre o bloqueio de filas. Você pode usar ArrayBlockingQueue, LinkedBlockingQueue, SynchronousQueue, PriorityBlockingQueue .
  6. threadFactory: A classe de engenharia para a criação de threads. Você pode definir um nome mais significativo para cada thread criado especificando a fábrica de threads.Se houver um problema de simultaneidade, também é conveniente encontrar a causa do problema.
  7. manipulador: Estratégia de saturação. Quando a fila de bloqueio do conjunto de encadeamentos está cheia e os encadeamentos especificados foram abertos, indicando que o conjunto de encadeamentos atual já está em um estado saturado, uma estratégia é necessária para lidar com essa situação. Existem várias estratégias adotadas:
    1. AbortPolicy: rejeita diretamente a tarefa enviada e lança uma exceção RejectedExecutionException ;
    2. CallerRunsPolicy: Use apenas o thread do chamador para realizar tarefas;
    3. DiscardPolicy: descarta a tarefa diretamente, sem processamento;
    4. DiscardOldestPolicy: descarta a tarefa armazenada por mais tempo na fila de bloqueio e executa a tarefa atual

Compartilhe mais sobre o desenvolvimento de back-end C / C ++ Linux dos princípios básicos de conhecimento e rede de aprendizagem para aprimorar os materiais de aprendizagem , pilha de tecnologia completa, conhecimento de conteúdo, incluindo Linux, Nginx, ZeroMQ, MySQL, Redis, pool de threads, MongoDB , ZK, streaming media, áudio e vídeo, kernel Linux, CDN, P2P, epoll, Docker, TCP / IP, corrotina, DPDK, etc.

Lógica de execução de pool de threads

Depois que o pool de threads é criado por meio de ThreadPoolExecutor, o processo de execução após a tarefa ser enviada, vamos dar uma olhada no código-fonte. O código-fonte do método execute é o seguinte:

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 as notas para a lógica de execução do método execute de ThreadPoolExecutor. A figura a seguir mostra o diagrama de execução do método execute de ThreadPoolExecutor:

 

Diagrama esquemático do processo de execução de execução.jpg

Existem várias situações na lógica de execução do método execute:

  1. Se os threads atualmente em execução forem menores que corePoolSize, novos threads serão criados para realizar novas tarefas;
  2. Se o número de threads em execução for igual ou maior que corePoolSize, as tarefas enviadas serão armazenadas na fila de bloqueio workQueue;
  3. Se a fila workQueue atual estiver cheia, um novo encadeamento será criado para executar a tarefa;
  4. Se o número de encadeamentos tiver excedido o máximoPoolSize, a estratégia de saturação RejectedExecutionHandler será usada para processamento.

Deve-se notar que a ideia de design do pool de threads é usar o pool de threads de núcleo corePoolSize, a fila de bloqueio workQueue e o pool de threads maximumPoolSize , como uma estratégia de cache para processar tarefas, de fato, essa ideia de design será usada no quadro.

5. Fechando o pool de threads

Para fechar o pool de threads, você pode usar os métodos shutdown e shutdownNow. Seu princípio é percorrer todos os threads no pool de threads e, em seguida, interromper os threads sucessivamente. Ainda há diferenças entre desligamento e desligamento Agora:

  1. shutdownNow primeiro define o estado do pool de threads para STOP , depois tenta parar todas as threads que estão executando e não executando tarefas e retorna a lista de tarefas esperando para serem executadas;
  2. O desligamento apenas define o estado do pool de threads para o estado SHUTDOWN e, em seguida, interrompe todos os threads que não estão realizando tarefas

Pode-se observar que o método shutdown continuará a execução da tarefa que está sendo executada, e o shutdownNow interromperá diretamente a tarefa que está sendo executada. Quando um desses dois métodos é chamado, o método isShutdown retornará verdadeiro. Quando todos os encadeamentos forem encerrados com êxito, isso significa que o pool de encadeamentos foi encerrado com êxito. Nesse momento, o método isTerminated retornará verdadeiro.

5. Como configurar os parâmetros do pool de threads de maneira razoável?

Se você deseja configurar o pool de threads razoavelmente, deve primeiro analisar as características da tarefa, que podem ser analisadas a partir das seguintes perspectivas:

  1. A natureza da tarefa: tarefas intensivas de CPU, tarefas intensivas de E / S e tarefas mistas.
  2. A prioridade da tarefa: alta, média e baixa.
  3. Tempo de execução da tarefa: longo, médio e curto.
  4. Dependência de tarefa: se depende de outros recursos do sistema, como conexões de banco de dados.

Tarefas com diferentes naturezas de tarefas podem ser processadas separadamente por conjuntos de threads de tamanhos diferentes. Configure tarefas que consomem muita CPU com o mínimo de threads possível, como configurar um pool de threads com threads Ncpu + 1 . Tarefas intensivas de E / S precisam esperar por operações de E / S, e os threads nem sempre estão realizando tarefas, portanto, configure o máximo de threads possível, como 2xNcpu . Para tarefas mistas, se eles puderem ser divididos, divida-os em uma tarefa com uso intensivo de CPU e uma tarefa com uso intensivo de E / S. Contanto que a diferença de tempo entre a execução das duas tarefas não seja muito grande, a taxa de transferência após a decomposição será ser maior Devido à taxa de transferência de execução serial, se o tempo de execução dessas duas tarefas diferir muito, não há necessidade de decompor. Podemos obter o número de CPUs do dispositivo atual por meio do método Runtime.getRuntime (). AvailableProcessors ().

Tarefas com prioridades diferentes podem ser processadas usando a fila de prioridade PriorityBlockingQueue. Permite que as tarefas de alta prioridade sejam executadas primeiro, deve-se observar que se sempre houver tarefas de alta prioridade enviadas para a fila, então as tarefas de baixa prioridade podem nunca ser executadas.

Tarefas com tempos de execução diferentes podem ser entregues a pools de threads de tamanhos diferentes para processamento, ou filas de prioridade podem ser usadas para permitir que tarefas com tempos de execução curtos sejam executadas primeiro.

Confie na tarefa do pool de conexão do banco de dados, porque o thread precisa esperar que o banco de dados retorne o resultado após o envio do SQL. Quanto maior o tempo de espera, maior o tempo ocioso da CPU e, então, maior será o número de threads definido, para que a CPU possa ser melhor utilizada.

Além disso, a fila de bloqueio é melhor usar uma fila limitada . Se uma fila ilimitada for usada, uma vez que a lista de pendências de tarefas está na fila de bloqueio, ela ocupará muitos recursos de memória e até mesmo causará o travamento do sistema.

Acho que você gosta

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