Fio
1. O conceito de fios
1.1 O que é um tópico
Thread é uma sequência de controle dentro do processo
Todos os processos devem ter pelo menos uma rota de execução
1.2 Processos e threads
Processo é a menor unidade de alocação de recursos
A menor unidade de execução de programa em um thread (agendamento)
Todas as informações do processo são compartilhadas pelo encadeamento, incluindo o texto do programa executável, a memória global e a pilha do programa, a memória heap e os descritores de arquivo, mas o encadeamento também tem alguns de seus próprios dados privados:
ID de thread, registro, pilha de execução, errno, palavra de máscara de sinal, prioridade de agendamento
por exemplo:
Por exemplo, a escola deseja realizar uma reunião esportiva e, em seguida, deve fazer várias coisas, compras, planejamento e assim por diante. Para fazer compras, o planejamento dessas coisas deve ser feito por pessoas diferentes ao mesmo tempo.
As escolas são como processos e as pessoas que fazem coisas diferentes são como fios.
【Nota】
Para o Linux, não há conceito de threads, todos são processos.
Assim, para a criação de um thread, um novo processo é criado e, em seguida, o PCB do processo e o PCB do processo anterior apontam para o mesmo espaço de endereço virtual. Dessa forma, os dados podem ser compartilhados e diferentes PCBs têm um desempenho diferente. Código.
O processo é chamado de processo leve
O tópico é criado com base no processo como modelo
1.3 Compartilhado por vários threads de um processo
Os threads estão todos no mesmo espaço de endereço, portanto, o Segmento de Texto e o Segmento de Dados são compartilhados.
Se você definir uma função, ela pode ser chamada em cada thread
Defina uma variável global, que pode ser acessada em todos os threads
Além disso, os tópicos também compartilham:
Tabela descritor de arquivo
Cada método de processamento de sinal
SIG_IGN (ignorado), SIG_DFL (padrão, geralmente termina o processo) ou função de processamento de sinal definida pelo usuário
Diretório de trabalho atual
ID de usuário e ID de grupo
1.4 Vantagens dos fios
O custo de criação de um novo thread é muito menor do que o custo de um processo
Porque a criação de um processo precisa abrir uma série de operações, como espaço de endereço virtual, tabela de página, PCB, etc. Para threads, recursos como espaço de endereço virtual e tabela de página já foram desenvolvidos e você só precisa desenvolver o PCB.
Em comparação com a alternância entre processos, alternar entre threads requer pouco trabalho do sistema operacional
Para alternar entre processos, você precisa alternar espaços de endereço virtual, tabelas de página, etc., mas para alternar thread, você só precisa alterar o PCB.
Threads ocupam recursos muito menores do que processos
A maioria dos recursos do thread são processos compartilhados e apenas uma pequena parte de seus próprios recursos privados. Tal como: ID de thread, um conjunto de registros, pilha de tempo de execução, errno, palavra de máscara de sinal, prioridade de agendamento
Pode fazer uso total do número de processadores paralelos
Para vários processadores, se houver apenas um processo, então apenas um processador estará funcionando e os outros processadores ficarão ociosos, o que é um desperdício de recursos para os processadores. Portanto, se você puder dividir o que um processo deve fazer, dividi-lo em vários e entregá-lo aos threads para fazer isso, ele fará uso total dos recursos do multi-processador
Enquanto aguarda o fim da operação de E / S lenta, o programa pode realizar outras tarefas
Por exemplo, para uma NetEase Cloud Music, você pode baixar uma música e depois ouvi-la. Este é um fenômeno causado por vários tópicos.
Aplicativos de computação intensiva, a fim de serem capazes de funcionar em um sistema multi-processador, o cálculo é dividido em vários threads para atingir
Para aplicações computacionalmente intensivas, a fim de melhorar sua eficiência computacional, o trabalho computacional pode ser dividido em várias partes, e então essas partes podem ser entregues a diferentes threads para cálculo, e várias threads são colocadas em diferentes processadores. O cálculo da memória pode melhorar a eficiência
Para aplicativos de I / O intensivos, a fim de melhorar o desempenho, as operações de I / O podem ser sobrepostas e os threads podem esperar por diferentes operações de I / O ao mesmo tempo
1.5 Desvantagens dos fios
Perda de desempenho
Robustez reduzida
Para um programa multi-threaded, a possibilidade de efeitos adversos devido a pequenos erros na alocação de tempo ou compartilhamento de variáveis que não deveriam ser compartilhadas é muito alta.
Falta de controle de acesso
O processo é a granularidade básica do controle de acesso. Chamar certas funções do sistema operacional em um thread afetará todo o processo
Maior dificuldade de programação
É muito mais complicado escrever e depurar um programa multithread do que um programa single-threaded
2. Controle de linha
Para controle de thread, devemos primeiro entender que estamos usando a biblioteca de thread POSIX.
Os nomes da maioria das funções da biblioteca de threads começam com "pthread"
Para usar esta biblioteca de threads, o arquivo de cabeçalho phread.h deve ser introduzido
A biblioteca de função de thread de link é usar o comando
-l pthread
2.1 ID do processo e ID do thread
No Linux. A implementação de thread atual é Native POSIX Thread Libaray, ou NPTL para breve. Nesta implementação, a thread também é conhecida como Light Weighted Process. Cada thread de modo de usuário corresponde a esta entidade de agendamento no kernel e também tem seu próprio descritor de processo (estrutura task_struct)
Antes dos threads, um processo corresponde a um descritor de processo no kernel. (1: 1)
Mas com threads, um processo corresponde a vários descritores de kernel. (1: N)
Mas para cada thread para chamar getpid para retornar o mesmo ID de processo, como resolver o problema acima?
Grupo de discussão:
struct task_struct { ... pid_t pid; tid_t tgid; ... struct task_struct *group_leader; ... struct list_head thread_group; ... };
Processos multi-threaded também são chamados de grupos de threads.Cada thread no grupo de threads tem um descritor de processo (task_struct) correspondente a ele no kernel.
O pid na estrutura do descritor do processo corresponde ao ID do thread
O tgid no descritor do processo corresponde ao ID do processo
Modo de usuário | Chamada de sistema | Estrutura correspondente do descritor de processo do kernel |
---|---|---|
ID do tópico | gettid (void) | pid_t pid |
Id de processo | getpid (void) | pid_t tgid |
Para o ID do segmento aprendido agora, é diferente do ID do segmento da biblioteca de segmentos POSIX. O tipo do ID do segmento é pthread_t. O ID do segmento e o ID do processo podem representar exclusivamente o segmento ou processo.
Como verificar o ID de um tópico?
ps -L
A opção -L mostrará
LWP: ID do thread, que é o valor de retorno da chamada do sistema gettid ()
NLWP: o número de threads no grupo de threads
Para o ID de thread, o Linux fornece uma chamada de sistema gettid para retornar o ID, mas glibc não encapsula a chamada de sistema para membros usarem em uma interface aberta.
O ID do tópico pode ser obtido das seguintes maneiras
#include <sys/syscall.h> pid_t tid; tid = syscall(STS_gettid);
O primeiro encadeamento no grupo de encadeamentos é chamado de encadeamento principal no modo do usuário e o líder do grupo no kernel. Quando o kernel cria o primeiro encadeamento, o ID do grupo de encadeamentos é definido como o encadeamento do primeiro ID, o ponteiro do líder do grupo aponta para si mesmo, o descritor do processo do thread principal.
Há um thread ID igual ao ID do processo no grupo de threads, e este thread é o thread principal do grupo de threads
【Nota】:
Threads e processos não são iguais, o processo tem um processo-pai de conceito, mas no grupo de threads dentro, todos os threads são relacionamentos de pares
2.2 Thread ID e layout do espaço de endereço do processo
A função pthread_create gera um ID de thread, que é armazenado no endereço apontado pelo primeiro parâmetro.
O ID do encadeamento é diferente do ID do encadeamento mencionado anteriormente.
O ID do encadeamento anterior pertence à categoria de agendamento de processo. Como um thread é um processo leve e a menor unidade do agendador do sistema operacional, um valor é necessário para representar o thread de maneira exclusiva.
O ID do thread gerado pela função pthread_create pertence à categoria da biblioteca de threads NPTL. O ID do thread é usado para outras funções da biblioteca de threads para usar o ID do thread para operar o thread
Biblioteca de thread NPTL, fornece a função pthread_self, você pode obter o ID do próprio thread
pthread_t pthread_self(void);
Para o tipo de pthread_t, na verdade, o ID do thread de pthread_t é essencialmente um endereço no espaço de endereço do processo
Como mostrado:
mmap: Você pode mapear um arquivo ou outro objeto na memória. Se o arquivo não for a soma do tamanho de várias páginas, o espaço não utilizado da última página será apagado.
3. Criar discussão
功能:创建一个新的线程 int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg); 参数: thread:返回线程的ID attr:设置线程的属性,attr为NULL表示使用默认的属性 start_routine:是一个函数地址,线程启动要执行的函数 arg:传给线程启动函数的参数 返回值: 成功返回0,失败返回错误码
Detecção de erro:
A diferença entre a função pthread e outras funções é que um erro retorna um código de erro
Para pthreads, ele também tem sua própria variável errno para suportar o código que usa errno. Mas para o erro da função pthread, geralmente é julgado pelo valor de retorno, porque o custo de leitura do valor de retorno é muito menor do que o custo de leitura do valor da variável errno no encadeamento
Exemplo:
#include <iostream> #include <pthread.h> #include <unistd.h> void* routine(void* arg) { int i = 10; while (i > 0) { std::cout << (char*)arg << '\n'; i--; } return NULL;//线程终止 } int main() { pthread_t tid; int ret = pthread_create(&tid, NULL, routine, (void*)("thread1")); if (ret > 0) { std::cout << "pthread_create error!" << '\n'; } sleep(1); int i = 30; while (i > 10) { std::cout << "I am main thread!" << '\n'; i--; } std::cout << "main exit!" << '\n'; return 0; }
3. Terminação de thread
Se você apenas encerrar um thread sem encerrar todo o processo, existem quatro métodos, como segue:
Retorne da função thread.
Este método não é aplicável ao thread principal, porque o retorno da função principal é equivalente a chamar exit
Um thread pode chamar pthread_cancel para encerrar outro thread no mesmo processo
O thread pode chamar a função pthread_exit para se encerrar
Você pode chamar a função pthread_kill para encerrar um tópico
Se qualquer thread chamar exit, _exit e Exit, todo o processo será encerrado.
3.1 pthread_exit
功能:线程终止 void pthread_exit(void* retval); 参数: retval:对于是joinable的线程,是其他线程调用pthread_join函数所得到的输出型参数的返回值 返回值: 无返回值,和进程一样,线程结束无法返回到他的调用者
Para retval:
Este é um parâmetro de saída, a informação retornada pela terminação do thread é colocada no espaço de memória apontado por retval
3.2 pthread_cancel
功能:向线程发送取消请求 int pthread_cancel(pthread_t thread); 参数: thread:需要取消的线程的ID 返回值: 成功返回0,失败返回错误码
3,3 pthread_kill
功能:向一个线程发送一个信号 #include <signal.h> int pthread_kill(pthread_t thread, int sig); 参数: thread:线程ID(pthread_create函数的输出型参数) sig:信号的编号 返回值: 成功返回0,失败返回错误码,并且没有信号发送
Exemplo:
#include <iostream> #include <pthread.h> #include <unistd.h> pthread_t main_tid; int retval; void* routine(void* arg) { pthread_cancel(main_tid); int i = 100000; pthread_detach(pthread_self());//自我分离 while (1) { std::cout << (char*)arg << ":" << i <<'\n'; i--; } pthread_exit((void*)&retval); //return NULL;//线程终止 } int main() { main_tid = pthread_self(); pthread_t tid; int ret = pthread_create(&tid, NULL, routine, (void*)("thread1")); if (ret > 0) { std::cout << "pthread_create error!" << '\n'; } sleep(5); int i = 30; while (i > 10) { std::cout << "I am main thread!" << '\n'; i--; } std::cout << "main exit!" << '\n'; while (1) { std::cout << "asd" << '\n'; } }
Este programa mostra que, após a saída do encadeamento principal, o encadeamento filho não precisa necessariamente sair , porque o processo não é encerrado , portanto, o encadeamento filho pode continuar em execução.
【Nota】:
A saída de um encadeamento pode afetar outro encadeamento porque a chave é ver se o encadeamento existente permitiu a saída do processo.Se o processo também sair, todos os outros encadeamentos devem sair.
Porque os threads são dependentes do processo
Para tópicos
Quando o kernel envia um sinal para a thread por meio do comando kill , o kernel adiciona o sinal a todo o grupo de threads por padrão
Portanto, para distinguir o sinal enviado para o processo ou o sinal enviado para o encadeamento, existem dois conjuntos de signal_pending em task_struct , um conjunto é comum ao grupo de encadeamento e o outro conjunto é para um único encadeamento .
Quando o sinal enviado por kill é colocado no signal_pengding compartilhado pelo grupo de threads , ele pode ser processado por qualquer thread . O sinal enviado por pthread_kill é colocado no signal_pending privado da thread e só pode ser processado pela thread.
4. Espera e separação da linha
4.1 thread em espera
Por que precisamos esperar por tópicos ? (Semelhante ao processo)
O segmento que saiu não liberou seu espaço e ainda está no espaço de endereço do processo
O novo thread criado não volta para reutilizar o espaço de endereço que acabou de sair
功能:等待线程结束 int pthread_join(pthread_t thread, void** retval); 参数: thread:线程的ID retval:他指向一个指针,后者指向线程的 返回值: 成功返回0,失败返回错误码
O encadeamento que chama esta função será suspenso (espera de bloqueio) até que o encadeamento com o encadeamento de ID termine. O encadeamento encerra de maneiras diferentes e o status de encerramento obtido por meio de pthraed_join é diferente.
Se o thread do thread retornar por retorno , a unidade apontada por retval armazena o valor de retorno da função do thread do thread
Se o thread do thread for encerrado de forma anormal por outro thread chamando pthread_cancel , a unidade apontada por rerval armazena a constante PTHREA_CANCELED
Se o thread do thread for encerrado chamando pthread_exit , a unidade apontada por retval armazena os parâmetros de pthread_exit
Se você não se preocupa com o status de encerramento do encadeamento , pode passar NULL para o parâmetro retval
Se o thread de discussão for encerrado chamando pthread_kill , a unidade apontada por retval armazena um valor aleatório
4.2 Separação de fios
Por padrão, os threads criados são todos juntáveis (combinados) , após a saída do thread, ela precisa ser uma operação pthread_join , caso contrário não será capaz de liberar recursos, resultando em vazamento de memória
Se você não se preocupa com o valor de retorno do thread, a junção é um fardo. Neste momento, podemos dizer ao sistema para liberar automaticamente os recursos do thread quando o thread sair.
int pthread_detach(pthread_t thread); int pthread_detach(pthread_self());
Podem ser outros threads no grupo de threads para separar o thread de destino ou o thread pode se separar
【Nota】:
juntável e separação é conflito, um thread é juntável nem isolado
#include <iostream> #include <pthread.h> #include <unistd.h> void* routine(void* arg) { pthread_detach(pthread_self()); std::cout << (char*)arg << '\n'; return NULL; } int main() { pthread_t tid; if (pthread_create(&tid, NULL, routine, (void*)("thread 1")) > 0) { std::cout << "pthread_create error!" << '\n'; } sleep(1); int ret = 0; if (pthread_join(tid, NULL) == 0) { std::cout << "wait success!" << '\n'; } else { std::cout << "wait error!" << '\n'; ret = 1; } return ret; }
Resultado:
thread 1 wait error
Explique que pthread_join e pthread_deatach não podem ser usados ao mesmo tempo, isso é um conflito
5. Sincronização de thread e exclusão mútua
Na maioria dos casos, as variáveis usadas pelos threads são variáveis locais, e o espaço de endereço da variável está no espaço do thread. Nesse caso, a variável pertence a uma única variável e outros threads não podem obter essa variável.
Mas, às vezes, muitas variáveis precisam ser compartilhadas dentro dos encadeamentos. Essas variáveis são chamadas de variáveis compartilhadas e a interação de vários encadeamentos pode ser concluída por meio do compartilhamento de dados.
A operação simultânea de variáveis compartilhadas por vários threads causará alguns problemas. Ou seja, é o problema do erro no reparo dos dados?
#include <iostream>
#include <pthread.h>
#include <unistd.h>
//出现6的情况是这样的,有可能下面创建线程创建到第六个出错的时候,i已经变成6了,然后对于buyTicket函数再次从内存中取值的时候,所以取到的就是6了
//多个线程来的时候是对这个函数的重入
void* buyTicket(void *arg)
{
int ticket = 20;
while (ticket > 0)
{
usleep(1000);
ticket--;
std::cout << (char*)(arg) << "buy ticket!Have Ticket:" << ticket << std::endl;
}
return NULL;//线程结束
}
int main()
{
pthread_t tid[6];
//不能用这种方法创建,因为buyTicket读取的是地址值,就会导致,每次读取的都是最新的i值,不会看到以前的线程了
//创建了五个子线程
//for (int i = 1; i < 6; i++)
//{
// if (pthread_create(tid + i, NULL, buyTicket, (void*)(&(i))) > 0)
// {
// std::cout << "pthread_create error!" << std::endl;
// }
// sleep(1);
//}
if (pthread_create(tid + 1, NULL, buyTicket, (void*)("1")) > 0)
{
std::cout << "pthread_create error!" << std::cout;
}
if (pthread_create(tid + 2, NULL, buyTicket, (void*)("2")) > 0)
{
std::cout << "pthread_create error!" << std::cout;
}
if (pthread_create(tid + 3, NULL, buyTicket, (void*)("3")) > 0)
{
std::cout << "pthread_create error!" << std::cout;
}
if (pthread_create(tid + 4, NULL, buyTicket, (void*)("4")) > 0)
{
std::cout << "pthread_create error!" << std::cout;
}
if (pthread_create(tid + 5, NULL, buyTicket, (void*)("5")) > 0)
{
std::cout << "pthread_create error!" << std::cout;
}
for (int i = 1; i <= 5; i++)
{
if (pthread_join(tid[i], NULL) == 0)
{
std::cout << i << " thread quit!" << std::endl;
}
sleep(1);
}
return 0;
}
-
Depois que a condição de julgamento while for verdadeira, o código pode mudar para outros processos simultaneamente
-
Para o último hibernar, o processo pode ser suspenso, o que significa que outros threads podem ter tempo suficiente para entrar na área crítica, e muitos threads podem entrar na área crítica.
-
--num
Em si não é uma operação atômica
- A operação não é atômica , mas corresponde a três instruções de montagem
Etapa 1: carregar a variável num em um registro na memória
Etapa 2: atualize o valor no registro e execute a operação -1
Etapa 3: Escreva o novo valor do registro de volta para o espaço de memória da variável num
Para resolver os problemas acima, três coisas precisam ser feitas:
O código deve ter comportamento mutuamente exclusivo: quando o código entra na área crítica para execução, outros processos não podem entrar na área crítica
Se vários threads exigirem a execução do código na seção crítica ao mesmo tempo, e não houver nenhum thread para executar na seção crítica neste momento, apenas um thread pode ter permissão para entrar na seção crítica
Se o thread não for executado na seção crítica, então o thread não pode impedir que outros threads entrem na seção crítica
Como mostrado:
5.1 Existem duas maneiras de inicializar o mutex:
Método um: alocação estática
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
Método dois: alocação dinâmica
int pthread_mutex_init(pthread_mutex_t* restrict mutex, const pthread_mutexattr* restrict attr); 参数: mutex:要初始化的互斥量 attr:NULL
5.2 destruir mutex
Destruir mutex
Precisa prestar atenção para destruir mutex:
Mutex inicializado com PTHREAD_MUTEX_INITALIZER não precisa ser destruído
Não destrua um mutex bloqueado
O mutex que foi destruído, certifique-se de que nenhum thread tentará travar novamente
int pthread_mutex_destory(pthread_mutex_t* mutex);
5.3 Bloqueio e desbloqueio de Mutex
int pthread_mutex_lock(pthread_mutex_t* mutex); int pthread_mutex_unlock(pthread_mutex_t* mutex); 返回值: 返回值为0,失败返回错误号
Ao chamar pthead_lock, você pode encontrar as seguintes situações:
O mutex está em um estado desbloqueado, esta função irá bloquear o mutex e retornar com sucesso
Quando a chamada de função é iniciada, outros threads bloquearam o mutex, ou existem outros threads aplicando-se ao mutex ao mesmo tempo, mas não há competição para o mutex, então a chamada pthread_lock será bloqueada, esperando que o mutex seja desbloqueado