Uma explicação detalhada de threads e pools de threads em alta simultaneidade

Tudo começa com a CPU

Você pode ter dúvidas, por que começar com a CPU quando se fala em multithreading? A razão é simples: não há conceitos da moda aqui e você pode ver a essência do problema com mais clareza . A CPU não conhece conceitos como threads e processos. A CPU sabe apenas duas coisas: 1. Buscar a instrução da memória 2. Executar a instrução e voltar para 1

Veja, aqui a CPU realmente não conhece os conceitos de processos e threads. A próxima pergunta é de onde a CPU busca as instruções? A resposta vem de um registrador chamado Program Counter (conhecido como PC), que é o conhecido contador de programas. Aqui, não pense no registrador de forma muito misteriosa. Você pode simplesmente entender o registrador como memória, mas a velocidade de acesso Apenas mais rápido. O que é armazenado no registro do PC? O que está armazenado aqui é o endereço da instrução na memória, qual instrução? é a próxima instrução que a CPU irá executar.

Então, quem vai definir o endereço de instrução no registrador do PC? Acontece que o endereço no registrador do PC é incrementado automaticamente em 1 por padrão, o que obviamente faz sentido, porque na maioria dos casos, a CPU executa um por um sequencialmente. Ao encontrar if e else, essa execução sequencial é interrompida. , quando a CPU executa este tipo de instrução, ela mudará dinamicamente o valor no registrador do PC de acordo com o resultado do cálculo, para que a CPU possa pular corretamente para a instrução que precisa ser executada. Inteligente, você definitivamente perguntará, como está o valor inicial no conjunto do PC? Antes de responder a esta pergunta, precisamos saber de onde vêm as instruções executadas pela CPU? Vem da memória, bobagem, as instruções na memória são carregadas do programa executável salvo no disco, o programa executável no disco é gerado pelo compilador e onde o compilador gera as instruções da máquina? A resposta é a função que definimos .

Observe que é uma função. Após a função ser compilada, ela formará a instrução executada pela CPU . Então, naturalmente, como fazemos a CPU executar uma função? Obviamente, só precisamos encontrar a primeira instrução formada após a compilação da função, e a primeira instrução é a entrada da função. Agora você deve saber que queremos que a CPU execute uma função, então só precisamos escrever o endereço da primeira instrução de máquina correspondente à função no registrador do PC , e a função que escrevemos começará a ser executada pela CPU. . Você pode ter dúvidas, o que isso tem a ver com fios?

Da CPU ao SO

Na seção anterior, entendemos o princípio de funcionamento da CPU. Se quisermos que a CPU execute uma determinada função, basta carregar a primeira execução da máquina correspondente à função no registrador do PC, para que possamos deixar a CPU executar a função mesmo sem um sistema operacional .programa de execução da CPU , embora viável, mas este é um processo muito trabalhoso, precisamos:

  • Encontre um carregador de região de tamanho adequado na memória
  • Encontre a entrada da função, defina o registro do PC e deixe a CPU começar a executar o programa

Essas duas etapas não são tão fáceis. Se o programador implementar manualmente os dois processos acima toda vez que o programa for executado, ele ficará louco, então programadores inteligentes irão querer simplesmente escrever um programa para completar automaticamente as duas etapas acima. etapa.

As instruções da máquina precisam ser carregadas na memória para execução, portanto, o endereço inicial e o comprimento da memória precisam ser registrados; ao mesmo tempo, o endereço de entrada da função deve ser encontrado e gravado no registrador do PC. uma estrutura de dados é necessária para registrar essas informações:

struct *** {
   void* start_addr;
   int len;
   
   void* start_point;
   ...
};

Então é hora de nomear. Essa estrutura de dados sempre deve ter um nome. Que informações essa estrutura deve registrar? O que é registrado é o estado de execução do programa quando ele é carregado na memória Qual é o nome do programa quando ele é carregado do disco para a memória? Basta chamá-lo de Processo . Nosso princípio orientador é que deve soar misterioso. Resumindo, não é fácil para todos entenderem. Eu o chamo de "o princípio da não compreensão ". E assim nasceu o processo. A primeira função executada pela CPU também tem um nome, a primeira função a ser executada parece mais importante, então vamos chamá-la apenas de função principal . O programa que completa as duas etapas acima também deve receber um nome.De acordo com o "não entendo o princípio", esse programa "simples" é chamado de sistema operacional (Sistema operacional). Assim nasceu o sistema operacional, e os programadores não precisam mais carregá-lo manualmente se quiserem rodar o programa. Agora que o processo e o SO estão prontos, tudo parece perfeito.

Do single-core ao multi-core, como fazer pleno uso do multi-core

Uma das características dos seres humanos é que a vida está constantemente mudando, de um núcleo para vários núcleos.

Neste momento, o que devemos fazer se quisermos escrever um programa e usar vários núcleos? Alguns alunos podem dizer que não há processo, basta abrir mais alguns processos? Parece razoável, mas há vários problemas:

  • Os processos precisam ocupar espaço de memória (veja isso na economia de energia anterior), se vários processos forem baseados no mesmo programa executável, o conteúdo das áreas de memória desses processos será quase idêntico, o que obviamente causará um desperdício de memória
  • As tarefas processadas pelo computador podem ser mais complicadas, o que envolve comunicação entre processos. Como cada processo está em um espaço de endereçamento de memória diferente, a comunicação entre processos naturalmente precisa da ajuda do sistema operacional, o que aumenta a dificuldade de programação e aumenta a complexidade do sistema. sobrecarga

O que podemos fazer sobre isso?

do processo ao thread

Deixe-me pensar sobre este problema com cuidado novamente. O chamado processo nada mais é do que uma seção de memória, que armazena as instruções de máquina executadas pela CPU e as informações da pilha quando a função está sendo executada . Para fazer o processo ser executado, coloque o função principal O endereço da primeira instrução de máquina é escrito no registrador do PC e o processo começa a ser executado.

A desvantagem de um processo é que existe apenas uma função de entrada, ou seja, a função principal, portanto as instruções de máquina no processo só podem ser executadas por uma CPU , então existe uma maneira de várias CPUs executarem as instruções de máquina em o mesmo processo? Inteligente, você deve ser capaz de pensar, já que podemos escrever o endereço da primeira instrução da função principal no registrador do PC, qual é a diferença entre outras funções e a função principal? A resposta é que não há diferença. A característica especial da função principal é que ela é a primeira função executada pela CPU. Não há nada de especial nisso. Podemos apontar o registrador do PC para a função principal e podemos apontar o registro do PC para qualquer função . Quando apontamos o registrador do PC para uma função não principal, nasce uma thread .

Até agora, liberamos nossas mentes.Pode haver várias funções de entrada em um processo, o que significa que as instruções de máquina pertencentes ao mesmo processo podem ser executadas por várias CPUs ao mesmo tempo . Observe que este é um conceito diferente de um processo. Ao criar um processo, precisamos encontrar uma área adequada na memória para carregar o processo e, em seguida, apontar o registrador do PC da CPU para a função principal, o que significa que há apenas uma execução fluxo no processo .

Mas agora é diferente, várias CPUs podem executar várias funções de entrada pertencentes ao processo ao mesmo tempo sob o mesmo teto (a área de memória ocupada pelo processo), ou seja, pode haver vários fluxos de execução em um processo agora .

Parece um pouco fácil de entender chamar sempre de fluxo de execução, e mais uma vez sacrificou o "não entendo o princípio", e deu um nome que não é fácil de entender, vamos chamar de thread. É aqui que entram os fios. O sistema operacional mantém um monte de informações para cada processo, que é usado para registrar o espaço de memória do processo, etc., e esse monte de informações é registrado como conjunto de dados A. Da mesma forma, o sistema operacional também precisa manter um monte de informações para o thread, que é usado para registrar a função de entrada ou informações de pilha do thread, e essa pilha de dados é registrada como conjunto de dados B. Obviamente, a quantidade do conjunto de dados B é menor que a dos dados A. Ao mesmo tempo, ao contrário de um processo, não há necessidade de encontrar um espaço de memória na memória ao criar um thread, porque o thread é executado no espaço de endereço do processo em que está localizado . Este espaço de endereço está no programa Ele foi criado na inicialização, e o thread é criado durante a execução do programa (após o início do processo), portanto, quando o thread começar a executar, este endereço o espaço já existe e o thread pode ser usado diretamente. É por isso que a criação de threads mencionadas em vários livros didáticos é mais rápida do que a criação de processos (claro que existem outros motivos). Vale ressaltar que com o conceito de threads, só precisamos criar várias threads depois que o processo for iniciado para manter todas as CPUs ocupadas, essa é a raiz do chamado alto desempenho e alta simultaneidade .

É muito simples, você só precisa criar um número apropriado de threads. Outro ponto digno de nota é que, como cada thread compartilha o espaço de endereço de memória do processo, a comunicação entre as threads não precisa depender do sistema operacional, o que traz grande comodidade aos programadores e problemas sem fim. o fato de que a comunicação entre threads é simplesmente muito conveniente para ser muito propensa a erros. A raiz do erro é que a CPU não tem o conceito de threads ao executar as instruções. Os problemas de exclusão mútua e sincronização enfrentados pela programação multi-threaded precisam ser resolvidos pelos próprios programadores. Há explicações detalhadas. A última coisa a ser lembrada é que, embora várias CPUs sejam usadas na figura anterior explicando o uso de threads, isso não significa que vários núcleos devem ser usados ​​para usar vários threads. No caso de um único núcleo, vários threads também podem ser criado. A razão é que o thread É implementado no nível do sistema operacional e não tem nada a ver com quantos núcleos existem . Quando a CPU executa instruções de máquina, ela não sabe a qual thread as instruções de máquina executadas pertencem . Mesmo quando há apenas uma CPU, o sistema operacional pode fazer com que cada thread avance "simultaneamente" por meio do escalonamento de threads. "Execute, mas na verdade ainda há apenas um thread em execução a qualquer momento.

 Informações por meio do trem: rota de aprendizado da tecnologia de código-fonte do kernel do Linux + tutorial em vídeo do código-fonte do kernel

Learning through train: Linux kernel code memory tuning file system process management device driver/network protocol stack

Fios e memória

Na discussão anterior, conhecemos a relação entre thread e CPU, ou seja, apontar o registrador PC da CPU para a função de entrada da thread, para que a thread possa executar, por isso devemos especificar uma função de entrada ao criar um fio. Independentemente da linguagem de programação usada, a criação de um thread é praticamente a mesma:

// 设置线程入口函数DoSomething
thread = CreateThread(DoSomething);

// 让线程运行起来
thread.Run();

Então, qual é a relação entre threads e memória? Sabemos que os dados gerados quando uma função é executada incluem informações como parâmetros da função , variáveis ​​locais e endereços de retorno . Essas informações são armazenadas na pilha. Quando o conceito de thread ainda não apareceu, existe apenas um fluxo de execução em o processo, portanto, há apenas uma pilha. , a parte inferior da pilha é a função de entrada do processo, ou seja, a função principal. Suponha que a função principal chame funA e funcA chame funcB, conforme mostrado na figura:

E depois de ter um fio? Depois de ter uma thread, existem várias entradas de execução em um processo, ou seja, existem vários fluxos de execução ao mesmo tempo, então um processo com apenas um fluxo de execução precisa de uma pilha para salvar informações de tempo de execução, então obviamente quando houver várias execuções fluxos, você precisa ter Várias pilhas são usadas para salvar as informações de cada fluxo de execução, o que significa que o sistema operacional deve alocar uma pilha para cada thread no espaço de endereçamento do processo , ou seja, cada thread possui sua própria pilha. É extremamente importante estar ciente disso.

Ao mesmo tempo, também podemos ver que a criação de threads consome espaço de memória do processo, o que também vale a pena observar.

uso de tópicos

Agora que temos o conceito de threads, como devemos usar threads como programadores a seguir? Do ponto de vista do ciclo de vida, existem dois tipos de tarefas a serem processadas por threads: tarefas longas e tarefas curtas. 1. Tarefas longas, tarefas de longa duração, como o nome sugere, significa que a tarefa sobrevive por muito tempo. Por exemplo, tomando como exemplo nossa palavra comumente usada, o texto que editamos no word precisa ser salvo no disco , e gravar dados no disco é uma tarefa. Então, um método melhor neste momento é criar um thread para gravar no disco. O ciclo de vida do thread de gravação é o mesmo do processo de texto. Contanto que o a palavra é aberta, o thread de escrita será criado. Quando o usuário fechar a palavra, o thread será destruído, esta é a tarefa longa.

Este cenário é muito adequado para criar threads dedicadas para lidar com algumas tarefas específicas, o que é relativamente simples. Existem tarefas longas e tarefas correspondentemente curtas.

2. Tarefas curtas, o conceito de tarefas de curta duração também é muito simples, ou seja, o tempo de processamento das tarefas é muito curto, como uma solicitação de rede, uma consulta ao banco de dados etc., essas tarefas podem ser processadas e concluídas rapidamente em pouco tempo. Portanto, tarefas curtas são mais comuns em vários servidores, como servidor web, servidor de banco de dados, servidor de arquivos, servidor de correio, etc. . Este cenário tem duas características: uma é que o tempo necessário para o processamento da tarefa é curto ; a outra é que o número de tarefas é grande . E se você fosse encarregado desse tipo de tarefa? Você pode pensar, isso é muito simples, quando o servidor recebe uma solicitação, ele cria um thread para processar a tarefa e destrói o thread após a conclusão do processamento, tão fácil. Esse método geralmente é chamado de thread por solicitação, o que significa que um thread é criado para uma solicitação:

Se for uma tarefa longa, este método pode funcionar muito bem, mas para um grande número de tarefas curtas, embora este método seja simples de implementar, ele tem várias desvantagens: 1. Como podemos ver nas seções anteriores, os threads são parte do conceito do sistema operacional (a implementação de threads de modo de usuário, corrotinas, etc. não são discutidas aqui), portanto, a criação de threads naturalmente precisa ser concluída com a ajuda do sistema operacional e a criação e destruição de threads pelo sistema operacional leva tempo. 2. Cada thread precisa ter sua própria pilha independente. Portanto, quando um grande número de threads é criado, muita memória e outros recursos do sistema serão consumidos. Isso é como se você fosse o proprietário de uma fábrica (pense nisso, você está feliz?) e você tem muitos pedidos em mãos. Cada vez que chega um lote de pedidos, você precisa recrutar um lote de trabalhadores. , os produtos produzidos são muito simples e os trabalhadores podem processá-los rapidamente. Depois de processar este lote de pedidos, esses trabalhadores que trabalharam tanto para recrutar serão demitidos. Quando houver novos pedidos, você trabalhará duro para recrutá-los. Trabalhadores uma vez, trabalham por 5 minutos e recrutam pessoas por 10 horas. Se você não está motivado para deixar a empresa falir, provavelmente não o fará. Portanto, uma estratégia melhor é recrutar um grupo de pessoas e criá-las na hora. Há pedidos Os pedidos são processados ​​quando há nenhuma ordem, e todos podem ficar ociosos quando não há ordem.

Esta é a origem do pool de encadeamentos.

Do multithreading ao pool de threads

O conceito do pool de threads é muito simples. Nada mais é do que criar um lote de threads e depois não mais liberá-los. As tarefas são submetidas a esses threads para processamento, portanto, não há necessidade de criar e destruir threads com frequência. ao mesmo tempo, devido ao número de threads no pool de threads Geralmente é fixo e não consome muita memória, então a ideia aqui é reutilizar e controlar .

Como funciona o pool de threads

Alguns alunos podem perguntar, como enviar tarefas para o pool de threads? Como essas tarefas são atribuídas aos encadeamentos no pool de encadeamentos? Obviamente, a fila na estrutura de dados é naturalmente adequada para esse cenário. O produtor que envia a tarefa é o produtor e a thread que consome a tarefa é o consumidor. Na verdade, esse é o clássico problema produtor- consumidor .

Agora você deve saber por que os cursos de sistema operacional são ministrados e as entrevistas são feitas sobre essa questão, porque se você não entender o problema produtor-consumidor, basicamente não poderá escrever o pool de encadeamentos corretamente. Por limitações de espaço, o blogueiro aqui não pretende explicar detalhadamente o problema produtor-consumidor, e a resposta pode ser obtida consultando as informações relevantes do sistema operacional. Aqui o blogueiro pretende falar sobre como são as tarefas geralmente submetidas ao pool de threads. De um modo geral, a tarefa submetida ao pool de threads consiste em duas partes: 1)  os dados a serem processados ; 2)  a função para processar os dados

struct task {
void* data;     // 任务所携带的数据
    handler handle; // 处理数据的方法
}

(Observe que você também pode entender a estrutura no código como uma classe, ou seja, um objeto.) Os encadeamentos no pool de encadeamentos serão bloqueados na fila. Quando o produtor grava dados na fila, um encadeamento no pool de encadeamentos will Após ser despertada, a thread retira a estrutura (ou objeto) acima da fila, pega os dados da estrutura (ou objeto) como parâmetro e chama a função de processamento:

while(true) {  struct task = GetFromQueue(); // 从队列中取出数据  task->handle(task->data);     // 处理数据}

O acima é a parte principal do pool de threads. Entenda isso e você poderá entender como o pool de threads funciona.

O número de threads no pool de threads

Agora que existe um pool de threads, qual é o número de threads no pool de threads? Pense nisso por si mesmo antes de prosseguir. Se você pode ver isso, você ainda não está dormindo. Você deve saber que poucos threads no pool de threads não podem fazer uso total da CPU, e muitos threads criados causarão degradação do desempenho do sistema, uso excessivo de memória, consumo causado pela troca de threads e assim por diante. Portanto, o número de threads não pode ser nem muito nem pouco, então quanto deve ser? Para responder a essa pergunta, você precisa saber com que tipos de tarefas o pool de threads lida.Alguns alunos podem dizer que você disse que existem dois tipos? Tarefas longas e tarefas curtas, isso é do ponto de vista do ciclo de vida, então também existem dois tipos do ponto de vista dos recursos necessários para processar as tarefas, que não são nada para procurar e abstrair. . Ah não, é intensivo em CPU e I/O. 1. Uso intensivo de CPU O chamado uso intensivo de CPU significa que as tarefas de processamento não precisam depender de E/S externa, como computação científica, operações de matriz e assim por diante. Nesse caso, desde que o número de threads seja basicamente o mesmo que o número de núcleos, os recursos da CPU podem ser totalmente utilizados.

2. Tarefas intensivas de E/S podem não levar muito tempo para a parte de cálculo, e a maior parte do tempo é gasta em E/S de disco, E/S de rede, etc.

Nesse caso é um pouco mais complicado, você precisa usar ferramentas de teste de desempenho para avaliar o tempo gasto na espera de I/O, aqui é registrado como WT (tempo de espera), e o tempo necessário para o cálculo da CPU, aqui é registrado como CT ( tempo de computação), então para um sistema N-core, o número apropriado de threads é provavelmente N * (1 + WT/CT), assumindo que o tempo de espera de E/S é o mesmo que o tempo de computação, então você precisa de cerca de 2N threads para utilizar totalmente os recursos da CPU, observe que este é apenas um valor teórico e a configuração específica precisa ser testada de acordo com cenários de negócios reais. É claro que fazer uso total da CPU não é o único ponto que precisa ser considerado. À medida que o número de threads aumenta, o uso de memória, o agendamento do sistema, o número de arquivos abertos, o número de soquetes abertos e os links de banco de dados abertos, todos precisam ser considerados. Portanto, aqui não existe uma fórmula universal, e ela precisa ser analisada caso a caso .

O pool de threads não é uma panacéia

O pool de threads é apenas uma forma de multi-threading, então os problemas enfrentados pelo multi-threading também são inevitáveis, como problemas de deadlock, problemas de condição de corrida, etc. sistema para obter a resposta. Portanto, a fundação é muito importante, ferros antigos.

Práticas recomendadas para uso do pool de threads

O pool de threads é uma arma poderosa nas mãos dos programadores. O pool de threads pode ser visto em quase todos os servidores de empresas de Internet. Antes de usar o pool de threads, você precisa considerar:

  • Compreenda totalmente sua tarefa, seja uma tarefa longa ou curta, seja intensiva em CPU ou I/O. pools, esta pode ser uma maneira melhor de determinar o número de threads
  • Se a tarefa no pool de encadeamentos tiver operações de E/S, certifique-se de definir um tempo limite para esta tarefa, caso contrário, o encadeamento que processa a tarefa pode ser bloqueado para sempre
  • É melhor não esperar pelos resultados de outras tarefas de forma síncrona no pool de threads

Resumir

Nesta seção, começamos da CPU e percorremos todo o caminho até o pool de threads comumente usado, da camada inferior à camada superior, do hardware ao software. Observe que não há linguagem de programação específica neste artigo. Threads não são um conceito de nível de linguagem (threads de modo de usuário ainda não são considerados), mas quando você realmente entende de threads, acredito que você pode usar muitos threads em qualquer idioma. você precisa entender é Tao, e então há arte. Espero que este artigo o ajude a entender os threads e os pools de threads.

Autor original: Code Farmer's Deserted Island Survival

 

Acho que você gosta

Origin blog.csdn.net/youzhangjing_/article/details/132190325
Recomendado
Clasificación