Compartilhamento de tecnologia

Linux multiprocesso e multithreading (8) Multithreading

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

Multithreading

Definição de tópico

Um thread é uma unidade de execução em um processo.

Responsável pela execução de programas no processo atual,

Há pelo menos um thread em um processo

Pode haver vários threads em um processo

Vários threads compartilham todos os recursos do mesmo processo e cada thread participa do agendamento unificado do sistema operacional

Pode ser entendido simplesmente como processo = recursos de memória + thread principal + thread filho +…

Tópicos e processos

Para tarefas intimamente relacionadas, o multithreading é preferido durante a simultaneidade. Para tarefas que não estão intimamente relacionadas e são relativamente independentes, é recomendado escolher multiprocessos;

  • Processo: A unidade básica de alocação de recursos pelo sistema operacional. É a menor unidade de alocação de recursos, a unidade de execução e agendamento do programa e a instância em execução do programa.
  • Thread: É a unidade básica de agendamento e despacho da CPU, a menor unidade de execução da CPU, a menor unidade do fluxo de execução do programa e a menor unidade de execução do programa.

A diferença entre threads e processos:

  • espaço de memória
    • Vários threads em um processo compartilham o mesmo espaço de memória
    • Vários processos têm espaço de memória independente
  • Comunicação entre processos/threads
    • Comunicação simples entre threads
    • A comunicação entre processos é complexa

Recursos de thread

  • Recursos de processo compartilhados
    • mesmo espaço de endereço
    • tabela de descritores de arquivo
    • Como cada sinal é processado
    • diretório de trabalho atual
    • ID do usuário e ID do grupo
  • Recursos exclusivos
    • Pilha de threads
    • Cada thread possui informações de contexto privadas
    • ID do tópico
    • Valor cadastrado
    • valor errado
    • Palavras de máscara de sinalização e prioridades de agendamento

Comandos relacionados ao thread

Existem muitos comandos para visualizar o processo no sistema Linux, incluindo pidstat, top, ps, você pode visualizar o processo, também pode visualizar um
tópico em processo

comando pidstat

Depois que a ferramenta sysstat precisar ser instalada no Ubuntu, o pidstat poderá ser suportado.

sudo apt instalar sysstat

Opções

-t: exibe os threads associados ao processo especificado

-p: Especifique o pid do processo

Exemplo

Veja os threads associados ao processo 12345

sudo pidstat -t -p 12345

Ver threads associados a todos os processos

sudo pidstat -t

Visualize os threads associados ao processo 12345, saída a cada 1 segundo

sudo pidstat -t -p 12345 1

Visualize os threads associados a todos os processos, produza a cada 1 segundo

sudo pidstat -t 1

comando superior

Use o comando top para visualizar os threads em um determinado processo. Você precisa usar a opção -H em combinação com -p para especificar o pid.

Opções

-H: Exibir informações do tópico

-p: Especifique o pid do processo

Exemplo

Veja os threads associados ao processo 12345

sudo top -H -p 12345

Ver threads associados a todos os processos

sudo topo -H

comando ps

Use o comando ps combinado com a opção -T para visualizar todos os threads em um processo

Opções

-T: Exibir informações do tópico

-p: Especifique o pid do processo

Exemplo

Veja os threads associados ao processo 12345

sudo ps -T -p 12345

Ver threads associados a todos os processos

sudo ps -T

Cenários comuns de simultaneidade

1. Modo multiprocesso

No modo multiprocesso, cada processo é responsável por tarefas diferentes sem interferir uns nos outros. Cada processo é executado em um espaço de memória diferente sem afetar um ao outro.

  • vantagem:
    • O espaço de endereço do processo é independente. Quando ocorre uma exceção em um processo, ela não afetará outros processos.
  • deficiência:
    • Cada processo precisa alocar espaço de memória independente. Criar um processo é caro e ocupa mais memória.
    • A colaboração entre processos e a comunicação entre processos são mais complexas
  • Cena aplicável:
    • Múltiplas tarefas não estão intimamente relacionadas, então o modo multiprocesso pode ser usado
    • Não há dependências entre tarefas e o modo multiprocesso pode ser usado

2. Modo multithread

No modo multithreading, pode haver vários threads em um processo, compartilhando o mesmo espaço de memória, e os threads podem se comunicar diretamente.

  • vantagem:
    • A comunicação entre threads é simples
    • Vários threads do mesmo processo podem compartilhar recursos e melhorar a utilização de recursos.
  • deficiência:
    • Threads não possuem espaços de endereço de processo independentes. Após a saída do thread principal, outros threads também serão encerrados.
    • A troca e o agendamento de threads requerem recursos. Muitos threads consumirão recursos do sistema.
    • A sincronização entre threads é complexa e questões de segurança de thread precisam ser consideradas
  • Cena aplicável:
    • Existem dependências entre tarefas e o modo multi-threading pode ser usado
    • A comunicação entre tarefas é relativamente frequente e o modo multithreading pode ser usado.

Criar tópico

1. pthread_create()

pthread_create() é usado para criar um thread. Após a criação ser bem-sucedida, o thread começa a ser executado.
Depois que pthread_create() for chamado com sucesso, ele retornará 0, caso contrário, retornará um código de erro.

Arquivo de cabeçalho de função:

#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg);
  • 1
  • 2
  • 3
  • 4

Descrição do parâmetro:

  • thread: Ponteiro para o tipo pthread_t, usado para armazenar o ID do thread.
  • attr: atributo Thread, que pode ser NULL, indicando que o atributo padrão é usado.
  • start_routine: Função de entrada do thread.
  • arg: Parâmetros passados ​​para a função de entrada do thread.

valor de retorno:

  • 0: Criado com sucesso.
  • EAGAIN: Recursos insuficientes, falha na criação do thread.
  • EINVAL: O parâmetro é inválido.
  • ENOMEM: Memória insuficiente, falha na criação do thread.

Perceber:

  • Depois que o thread filho for criado com sucesso, ele será agendado de forma independente para execução e executado simultaneamente com outros threads.
  • A biblioteca -lpthread precisa ser vinculada durante a compilação.

Exemplo: Criar um tópico

// todo : 创建一个线程,并在线程中打印出“Hello, World!”
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

// 线程函数
//@param arg 线程函数参数
void * print_hello(void *arg) {
    printf("%sn",(char *)arg);
}

int main() {
    pthread_t tid; //? typedef unsigned long int pthread_t;
    // 创建线程
    //@param tid 线程ID
    //@param attr 线程属性
    //@param start_routine 线程函数
    //@param arg 线程函数参数
    int ret = pthread_create(&tid, NULL,print_hello, "Hello, World!");
    if (ret!= 0){
        printf("pthread_create error!n");
        return 1;
    }
    sleep(1); // 等待线程执行完毕
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

2. pthread_exit() sai do tópico

pthread_exit() é usado para sair do thread. Depois que o thread concluir a execução, pthread_exit() será chamado automaticamente para sair.

Arquivo de cabeçalho de função:

#include <pthread.h>

void pthread_exit(void *retval);
  • 1
  • 2
  • 3

Descrição do parâmetro:

  • retval: o valor retornado quando o thread é encerrado.
  • Após a execução da função thread, pthread_exit() será automaticamente chamado para sair.

3. pthread_join() espera o thread terminar

pthread_join() é usado para esperar o thread terminar,
Após chamar pthread_join(), o thread atual será bloqueado até que o thread termine.

Arquivo de cabeçalho de função:

#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);
  • 1
  • 2
  • 3

Descrição do parâmetro:

  • tópico: ID do tópico.
  • retval: Ponteiro para o valor de retorno do thread, usado para armazenar o valor retornado quando o thread termina. (ponteiro secundário)

valor de retorno:

  • 0: Espere pelo sucesso.
  • EINVAL: O parâmetro é inválido.
  • ESRCH: ID do thread não existe.
  • EDEADLK: O thread está em estado de deadlock.

Exemplo:

// todo : 创建一个线程,并在线程中打印出“Hello, World!”
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

// 线程函数
//@param arg 线程函数参数
void * print_hello(void *arg) {
    sleep(1); // 休眠1秒
    printf("%sn",(char *)arg);
    pthread_exit(NULL); // 线程退出
}

int main() {
    pthread_t tid; //? typedef unsigned long int pthread_t;
    // 创建线程
    //* @param tid 线程ID
    //* @param attr 线程属性
    //* @param start_routine 线程函数
    //* @param arg 线程函数参数
    int ret = pthread_create(&tid, NULL,print_hello, "Hello, World!");
    if (ret!= 0){
        printf("pthread_create error!n");
        return 1;
    }

    printf("等待线程结束...n");
    // 等待线程结束
    //* @param thread 线程ID
    //* @param status 线程退出状态
    pthread_join(tid, NULL);

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
等待线程结束...
Hello, World!
  • 1
  • 2

Separação de threads

Os fios são divididos em combináveis ​​​​e destacáveis

  • Pode ser combinado
    • Um thread combinável pode recuperar seus recursos e ser eliminado por outros threads; seus recursos de memória (como a pilha) não são liberados até que sejam recuperados por outros threads;
    • O estado padrão de criação do thread é combinável. A função pthread_join pode ser chamada por outros threads para aguardar a saída do thread filho e liberar recursos relacionados.
  • separável
    • Ele não pode ser reciclado ou eliminado por outros threads. Os recursos do thread são liberados pelo sistema quando ele termina.
// todo : 创建一个线程,并在线程中打印出“Hello, World!”
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

// 线程函数
//@param arg 线程函数参数
void * print_hello(void *arg) {
    sleep(1); // 休眠1秒
    printf("%sn",(char *)arg);
    pthread_exit(NULL); // 线程退出
}

int main() {
    pthread_t tid; //? typedef unsigned long int pthread_t;
    // 创建线程
    //* @param tid 线程ID
    //* @param attr 线程属性
    //* @param start_routine 线程函数
    //* @param arg 线程函数参数
    int ret = pthread_create(&tid, NULL,print_hello, "Hello, World!");
    if (ret!= 0){
        printf("pthread_create error!n");
        return 1;
    }

    printf("等待线程结束...n");
    // 等待线程结束
    //* @param thread 线程ID
    //* @param status 线程退出状态
    //pthread_join(tid, NULL);//! 阻塞等待线程结束,直到线程结束后才继续往下执行

    //线程分离
    pthread_detach(tid); //! 分离线程,不用等待线程结束后才退出程序,该线程的资源在它终止时由系统来释放。

    printf("主线程结束n");
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

Crie vários tópicos

Exemplo 1: Crie vários threads para executar tarefas diferentes

// todo : 创建多个线程,执行不同的任务
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

// 线程函数
//@param arg 线程函数参数
void * print_hello_A(void *arg) {
    sleep(1); // 休眠1秒
    printf("%sn",(char *)arg);
    pthread_exit(NULL); // 线程退出
}
// 线程函数
//@param arg 线程函数参数
void * print_hello_B(void *arg) {
    sleep(2); // 休眠2秒
    printf("%sn",(char *)arg);
    pthread_exit(NULL); // 线程退出
}


int main() {
    pthread_t tidA; //? 存储线程ID  typedef unsigned long int pthread_t;
    pthread_t tidB;
    // 创建线程
    //* @param tid 线程ID
    //* @param attr 线程属性
    //* @param start_routine 线程函数
    //* @param arg 线程函数参数
    int retA = pthread_create(&tidA, NULL,print_hello_A, "A_ Hello, World!");
    if (retA!= 0){
        printf("pthread_create error!n");
        return 1;
    }

    int retB = pthread_create(&tidB, NULL,print_hello_B, "B_ Hello, World!");
    if (retB!= 0){
        printf("pthread_create error!n");
        return 1;
    }
    
    printf("等待线程结束...n");
    // 等待线程结束
    //* @param thread 线程ID
    //* @param status 线程退出状态
    pthread_join(tidA, NULL);//! 阻塞等待线程结束,直到线程结束后才继续往下执行
    pthread_join(tidB, NULL);
    printf("主线程结束n");
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

Exemplo 2: Crie vários threads para executar a mesma tarefa

// todo : 创建多个线程,执行相同任务
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
//? 两个线程执行相同任务,对函数中的值修改了,会不会影响到其他线程的执行?
//! 在多线程编程中,如果多个线程执行相同的任务并且对共享资源进行修改,可能会影响到其他线程的执行。
//! 这是因为多个线程共享相同的内存空间,对共享资源的修改可能会导致竞态条件(race condition),
//! 从而导致不可预测的行为。
//! print_hello函数中的变量i是局部变量,每个线程都会有自己的i副本,因此对i的修改不会影响到其他线程。
//! 但是,如果涉及到共享资源(例如全局变量或静态变量),就需要考虑线程同步的问题,以避免竞态条件。


//*局部变量:每个线程都有自己的栈空间,因此局部变量是线程私有的,不会影响到其他线程。
//*共享资源:如果多个线程访问和修改同一个全局变量或静态变量,就需要使用同步机制(如互斥锁、信号量等)来确保线程安全。
//Linux:在Linux系统中,默认的线程栈大小通常是8MB。可以使用ulimit -s命令查看和修改当前用户的线程栈大小。例如,ulimit -s 1024将线程栈大小设置为1MB。
//Windows:在Windows系统中,默认的线程栈大小是1MB。可以通过编译器选项或在创建线程时指定栈大小来修改。

// 线程函数
//@param arg 线程函数参数
void * print_hello(void *arg) {

    for (char i = 'a'; i < 'z' ; ++i) {
        printf("%cn", i);
        sleep(1); // 休眠1秒
    }
    pthread_exit(NULL); // 线程退出
}

int main() {
    pthread_t tid[2]={0}; //? 存储线程ID的数组  typedef unsigned long int pthread_t;


    for (int i = 0; i < 2; ++i) {
        // 创建线程
        //* @param tid 线程ID
        //* @param attr 线程属性
        //* @param start_routine 线程函数
        //* @param arg 线程函数参数
        int retA = pthread_create(&tid[i], NULL,print_hello, NULL);
        if (retA!= 0){
            printf("pthread_create error!n");
            return 1;
        }
    }

    printf("等待线程结束...n");
    // 等待线程结束
    //* @param thread 线程ID
    //* @param status 线程退出状态
    pthread_join(tid[0], NULL);//! 阻塞等待线程结束,直到线程结束后才继续往下执行
    pthread_join(tid[1], NULL);


    printf("主线程结束n");
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

Comunicação entre threads

Outra comunicação entre processos também se aplica à comunicação entre threads.

O thread principal passa parâmetros para o thread filho

Ao criar um thread filho por meio da função pthread_create(), o quarto parâmetro de pthread_create() é o parâmetro passado para a função do thread filho.

Subthread passa parâmetros para thread principal

Ao sair do thread filho por meio da função pthread_exit(), você pode passar parâmetros para o thread principal.

void pth_exit(void *retval);
  • 1

Ao aguardar o final do subthread através da função pthread_join(), obtenha os parâmetros de retorno do subthread.

int pthread_join (pthread_t __th, void **__thread_return);
//二级指针获取到了pthread_exit()函数参数指针的指向地址,通过该地址可以获取到子线程的返回参数。
  • 1
  • 2

Exemplo:

// todo : 线程直接通讯,子线程向父线程传参
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

// 线程函数
//@param arg 线程函数参数
void * print_hello(void *arg) {
    printf("子线程开始,结束之时传递参数100的地址n");

    sleep(1); // 休眠1秒
    //! int num=100;//局部变量,函数结束释放内存
    static int num=100;//* 静态局部变量,函数结束不释放内存,延长生命周期
    pthread_exit(&num); // 线程退出
}



int main() {
    pthread_t tid; //? 存储线程ID  typedef unsigned long int pthread_t;
    // 创建线程
    //* @param tid 线程ID
    //* @param attr 线程属性
    //* @param start_routine 线程函数
    //* @param arg 线程函数参数
    int retA = pthread_create(&tid, NULL,print_hello, NULL);
    if (retA!= 0){
        printf("pthread_create error!n");
        return 1;
    }



    printf("等待线程结束...n");
    void* num;//获取子进程传递的参数,num指向了子进程传递的参数
    // 等待线程结束
    //* @param thread 线程ID
    //* @param status 线程退出状态
    pthread_join(tid, (void **)&num);//! 阻塞等待线程结束,直到线程结束后才继续往下执行
    printf("子线程结束,传递的参数为%dn",*(int*)num);
    printf("主线程结束n");
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

Mutex de thread

Mutex de thread

Mutex é um mecanismo de sincronização usado para controlar o acesso a recursos compartilhados.

A principal vantagem dos threads é a capacidade de compartilhar informações através de variáveis ​​globais, mas esse compartilhamento conveniente tem um custo:

Deve ser garantido que vários threads não modifiquem a mesma variável ao mesmo tempo

Um determinado thread não lerá variáveis ​​​​modificadas por outros threads. Na verdade, dois threads não podem acessar a seção crítica ao mesmo tempo.

O princípio do bloqueio mutex

O princípio de um bloqueio mutex é que quando um thread tenta entrar em uma área mutex, se a área mutex já estiver ocupada por outros threads, o thread será bloqueado até que a área mutex seja liberada.

É essencialmente uma variável do tipo pthread_mutex_t, que contém um valor inteiro para representar o status da área mutex.
Quando o valor é 1, significa que o recurso crítico atual pode competir pelo acesso e o thread que obtém o bloqueio mutex pode entrar na seção crítica. Neste momento o valor é 0 e outros threads só podem esperar.
Quando o valor é 0, significa que o recurso crítico atual está ocupado por outros threads e não pode entrar na seção crítica e apenas esperar.

Características dos bloqueios mutex

typedef union
{
  struct __pthread_mutex_s __data; // 互斥锁的结构体
  char __size[__SIZEOF_PTHREAD_MUTEX_T];// 互斥锁的大小
  long int __align;// 互斥锁的对齐
} pthread_mutex_t;

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • O bloqueio mutex é uma variável do tipo pthread_mutex_t, que representa um bloqueio mutex.
  • Se dois threads acessam a mesma variável pthread_mutex_t, então eles acessam o mesmo bloqueio mutex
  • As variáveis ​​correspondentes são definidas no arquivo de cabeçalho pthreadtypes.h, que é uma união contendo uma estrutura.

Uso de bloqueios mutex

Existem duas maneiras principais de inicializar bloqueios mutex de thread:

inicialização estática

  • Defina uma variável do tipo pthread_mutex_t e inicialize-a como PTHREAD_MUTEX_INITIALIZER.
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER
  • 1

inicialização dinâmica

Inicialização dinâmica A inicialização dinâmica envolve principalmente duas funções: função pthread_mutex_init e função pthread_mutex_destroy

Função pthread_mutex_init()

Usado para inicializar um bloqueio mutex, aceita dois parâmetros: o endereço do bloqueio mutex e os atributos do bloqueio mutex.

Arquivo de cabeçalho de função:

#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
  • 1
  • 2
  • 3

Descrição do parâmetro:

  • mutex: Ponteiro para o tipo pthread_mutex_t, usado para armazenar o endereço do bloqueio mutex.
  • attr: o atributo do bloqueio mutex, que pode ser NULL, indicando que o atributo padrão é utilizado.

valor de retorno:

  • 0: Inicialização bem-sucedida.
  • Um código de erro é retornado em caso de falha.
Função pthread_mutex_destroy()

Usado para destruir o bloqueio mutex, aceita um parâmetro: o endereço do bloqueio mutex.

Arquivo de cabeçalho de função:

#include <pthread.h>

int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • 1
  • 2
  • 3

Descrição do parâmetro:

  • mutex: Ponteiro para o tipo pthread_mutex_t, usado para armazenar o endereço do bloqueio mutex.

valor de retorno:

  • 0: Destruição bem-sucedida.
  • Um código de erro é retornado em caso de falha.

Exemplo:

// todo :  互斥锁;创建两个线程,分别对全局变量进⾏ +1 操作
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>


static int global = 0;// 全局变量

//静态初始化互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;// 互斥锁
//动态初始化互斥锁
pthread_mutex_t mut;// 互斥锁

// 线程函数
//@param arg 线程函数参数
void * print_hello(void *arg) {
    printf("子线程开始n");

    int loops = *(int *)arg;
    int i,tmp = 0;
    for (i = 0;i < loops;i++){
        pthread_mutex_lock(&mut);// 加锁
        printf("子线程%d,global=%dn",i,global);
        tmp = global;
        tmp++;
        global = tmp;
        pthread_mutex_unlock(&mut);// 解锁
    }
    printf("子线程结束n");
    pthread_exit(NULL); // 线程退出
}



int main() {

    // 动态初始化互斥锁
    //* @param mutex 互斥锁
    //* @param attr 互斥锁属性 NULL 是默认属性
    int r= pthread_mutex_init(&mut,NULL);
    if (r!= 0){
        printf("pthread_mutex_init error!n");
        return 1;
    }

    pthread_t tid[2]={0}; //? 存储线程ID  typedef unsigned long int pthread_t;
    int arg=20;
    for (int i = 0; i < 2; i++){
        // 创建线程
        //* @param tid 线程ID
        //* @param attr 线程属性
        //* @param start_routine 线程函数
        //* @param arg 线程函数参数
        int retA = pthread_create(&tid[i], NULL,print_hello, &arg);
        if (retA!= 0){
            printf("pthread_create error!n");
            return 1;
        }
    }




    printf("等待线程结束...n");
    // 等待线程结束
    //* @param thread 线程ID
    //* @param status 线程退出状态
    pthread_join(tid[0],NULL );//! 阻塞等待线程结束,直到线程结束后才继续往下执行
    pthread_join(tid[1],NULL );

    printf("%dn",global);
    printf("主线程结束n");

    // 销毁动态创建的互斥锁
    //* @param mutex 互斥锁
    pthread_mutex_destroy(&mut);// 销毁互斥锁

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79

Sincronização de threads

Sincronização de threads: refere-se ao acesso ordenado aos recursos pelos visitantes através de outros mecanismos com base na exclusão mútua (na maioria dos casos).

Variável de condição: um mecanismo fornecido pela biblioteca de threads especificamente para sincronização de threads.

Um cenário típico de aplicação para sincronização de threads é entre produtores e consumidores.

Questões do produtor e do consumidor

Neste modelo, ele é dividido em thread produtor e thread consumidor, e o processo de sincronização de múltiplos threads é simulado por meio desse thread.

Neste modelo são necessários os seguintes componentes:

  • Armazém: usado para armazenar produtos, geralmente como um recurso compartilhado
  • Thread do produtor: usado para produzir produtos
  • Segmento de consumo: usado para consumir produtos

princípio:

Quando não há produto no armazém, o segmento consumidor precisa esperar até que haja um produto antes de poder consumi-lo.

Quando o armazém está cheio de produtos, o segmento produtor precisa esperar até que o segmento consumidor consuma os produtos.

Exemplo de implementação de modelos de produtor e consumidor baseados em bloqueios mutex

O fio condutor é o consumidor

n subthreads como produtores

// todo :  基于互斥锁实现⽣产者与消费者模型主线程为消费者,n 个⼦线程作为⽣产者
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
static int n = 0; // 产品数量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;// 互斥锁

//生产者执行函数
void * dofunc(void *arg) {
    int arg1 = *(int*)arg;
    for (int i = 0; i <arg1; i++) {
        //获取互斥锁
        pthread_mutex_lock(&mutex);
        //生产产品
        printf("生产者%ld生产了%d个产品n",pthread_self(),++n);//! pthread_self()返回当前线程ID
        //释放互斥锁
        pthread_mutex_unlock(&mutex);
        //休眠1秒
        sleep(1);
    }
    pthread_exit(NULL);
}


int main() {
    pthread_t tid[4]={0}; //? 存储线程ID  typedef unsigned long int pthread_t;
    int arr[4]={1,2,3,4};
    for (int i = 0; i < 4; i++) {
        // 创建线程
        //* @param tid 线程ID
        //* @param attr 线程属性
        //* @param start_routine 线程函数
        //* @param arg 线程函数参数
        int retA = pthread_create(&tid[i], NULL,dofunc,&arr[i] );
        if (retA!= 0){
            printf("pthread_create error!n");
            return 1;
        }
    }
    //消费者执行

    for (int i = 0;i<10;i++) {
        //获取互斥锁
        pthread_mutex_lock(&mutex);
        while (n > 0){
            //消费产品
            printf("消费者%ld消费了1个产品:%dn",pthread_self(),n--);
        }
        //释放互斥锁
        pthread_mutex_unlock(&mutex);
        //休眠1秒
        sleep(1);
    }


    printf("等待线程结束...n");
    // 等待线程结束
    //* @param thread 线程ID
    //* @param status 线程退出状态
    pthread_join(tid[0],NULL );//! 阻塞等待线程结束,直到线程结束后才继续往下执行
    pthread_join(tid[1],NULL );
    pthread_join(tid[2],NULL );
    pthread_join(tid[3],NULL );

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67

variável de condição

Uma variável de condição é um mecanismo de sincronização que permite que um thread espere que uma determinada condição seja atendida antes de continuar a execução.

O princípio de uma variável de condição é que ela contém um bloqueio mutex e uma fila de espera.

Os bloqueios mutex são usados ​​para proteger filas de espera e variáveis ​​de condição.

Insira a descrição da imagem aqui

Inicialização de variável de condição

A natureza da variável de condição é do tipo pthread_cond_t

其他线程可以阻塞在这个条件变量上, 或者唤
醒阻塞在这个条件变量上的线程
typedef union
{
  struct __pthread_cond_s __data;
  char __size[__SIZEOF_PTHREAD_COND_T];
  __extension__ long long int __align;
} pthread_cond_t;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

A inicialização de variáveis ​​de condição é dividida em inicialização estática e inicialização dinâmica.

inicialização estática

Para variáveis ​​de condição inicializadas estaticamente, você precisa primeiro definir uma variável do tipo pthread_cond_t e, em seguida, inicializá-la como PTHREAD_COND_INITIALIZER.

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
  • 1

Inicialização dinâmica pthread_cond_init()

Para inicializar dinamicamente uma variável de condição, você precisa primeiro definir uma variável do tipo pthread_cond_t e, em seguida, chamar a função pthread_cond_init para inicializá-la.

Arquivo de cabeçalho de função:

#include <pthread.h>

int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
  • 1
  • 2
  • 3

Descrição do parâmetro:

  • cond: Ponteiro para o tipo pthread_cond_t, usado para armazenar o endereço da variável de condição.
  • attr: atributo da variável de condição, que pode ser NULL para utilizar o atributo padrão.

valor de retorno:

  • 0: Inicialização bem-sucedida.
  • Um código de erro é retornado em caso de falha.

pthread_cond_destroy()

Usado para destruir variáveis ​​de condição, aceita um parâmetro: o endereço da variável de condição.

Arquivo de cabeçalho de função:

#include <pthread.h>

int pthread_cond_destroy(pthread_cond_t *cond);
  • 1
  • 2
  • 3

Descrição do parâmetro:

  • cond: Ponteiro para o tipo pthread_cond_t, usado para armazenar o endereço da variável de condição.

valor de retorno:

  • 0: Destruição bem-sucedida.
  • Um código de erro é retornado em caso de falha.

Uso de variáveis ​​de condição

O uso de variáveis ​​de condição é dividido em espera e notificação

Aguarde pthread_cond_wait()

A função de espera pthread_cond_wait() aceita três parâmetros: o endereço da variável de condição, o endereço do bloqueio mutex e o tempo de espera.

Arquivo de cabeçalho de função:

#include <pthread.h>

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
  • 1
  • 2
  • 3

Descrição do parâmetro:

  • cond: Ponteiro para o tipo pthread_cond_t, usado para armazenar o endereço da variável de condição.
  • mutex: Ponteiro para o tipo pthread_mutex_t, usado para armazenar o endereço do bloqueio mutex.
  • abstime: timeout, que pode ser NULL, indicando que não há timeout.

valor de retorno:

  • 0: Espere pelo sucesso.
  • Um código de erro é retornado em caso de falha.

Notificar pthread_cond_signal()

função de notificação
pthread_cond_signal() aceita um parâmetro: o endereço da variável de condição.

Arquivo de cabeçalho de função:

#include <pthread.h>

int pthread_cond_signal(pthread_cond_t *cond);
  • 1
  • 2
  • 3

Descrição do parâmetro:

  • cond: Ponteiro para o tipo pthread_cond_t, usado para armazenar o endereço da variável de condição.

valor de retorno:

  • 0: Notificação bem-sucedida.
  • Um código de erro é retornado em caso de falha.

Notificar todos pthread_cond_broadcast()

Notificar todas as funções
pthread_cond_broadcast() aceita um parâmetro: o endereço da variável de condição.

Arquivo de cabeçalho de função:

#include <pthread.h>

int pthread_cond_broadcast(pthread_cond_t *cond);
  • 1
  • 2
  • 3

Descrição do parâmetro:

  • cond: Ponteiro para o tipo pthread_cond_t, usado para armazenar o endereço da variável de condição.

valor de retorno:

  • 0: Notificação bem-sucedida.
  • Um código de erro é retornado em caso de falha.

Exemplo de implementação de modelos de produtor e consumidor baseados em variáveis ​​condicionais

Insira a descrição da imagem aqui

step 1 : 消费者线程判断消费条件是否满足 (仓库是否有产品),如果有产品可以消费,则可以正
常消费产品,然后解锁
step 2 : 当条件不能满足时 (仓库产品数量为 0),则调用 pthread_cond_wait 函数, 这个函数
            具体做的事情如下:
            在线程睡眠之前,对互斥锁解锁
            让线程进⼊到睡眠状态
            等待条件变量收到信号时 唤醒,该函数重新竞争锁,并获取锁后,函数返回 
step 3 :重新判断条件是否满足, 如果不满足,则继续调用 pthread_cond_wait 函数
step 4 : 唤醒后,从 pthread_cond_wait 返回,消费条件满足,则正常消费产品
step 5 : 释放锁,整个过程结束
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

Por que as variáveis ​​de condição precisam ser usadas em conjunto com bloqueios mutex?

Proteja os dados compartilhados:

Os bloqueios mutex são usados ​​para proteger dados compartilhados e garantir que apenas um thread possa acessar e modificar os dados ao mesmo tempo.

Isso evita corridas e inconsistências de dados.

Variáveis ​​de condição são usadas para comunicação entre threads para notificar outros threads de que uma determinada condição foi atendida.

No entanto, a operação de variáveis ​​de condição por si só não fornece proteção para dados compartilhados, por isso precisa ser usada em conjunto com um bloqueio mutex.

Evite falsos despertares:

Uma característica das variáveis ​​de condição é que pode ocorrer uma ativação espúria.

Ou seja, o thread é despertado sem notificação explícita. Para evitar operações incorretas causadas por esta situação,

O thread precisa verificar novamente se a condição foi realmente atendida após acordar.

O uso de um mutex garante que os dados compartilhados não sejam modificados por outros threads durante a verificação das condições, evitando assim erros causados ​​por ativações falsas.

Verifique se a notificação está correta:

Quando um thread notifica outros threads por meio de variáveis ​​de condição, ele precisa garantir que os dados compartilhados foram atualizados antes de notificar.

Um mutex garante isso, garantindo que todas as operações de atualização de dados foram concluídas antes de liberar o bloqueio.

Da mesma forma, o thread que recebe a notificação precisa manter o mutex antes de verificar a condição para garantir que os dados estejam estáveis ​​quando a condição for verificada.

Implemente padrões de sincronização complexos:
A combinação de bloqueios mutex com variáveis ​​de condição pode implementar padrões de sincronização mais complexos, como problemas de produtor-consumidor, problemas de leitor-escritor, etc. Mutexes protegem dados compartilhados e variáveis ​​de condição são usadas para coordenação e comunicação entre threads.

// todo :  条件变量
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdbool.h>
#include <stdlib.h>


static int number = 0;// 产品数量
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;// 互斥锁
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;// 条件变量

// 线程函数
//@param arg 线程函数参数
void * thread_handler(void *arg) {
    int cnt = atoi((char *)arg);// 获取线程参数
    int i,tmp;// 临时变量
    for(i = 0;i < cnt;i++){// 生产产品
        pthread_mutex_lock(&mtx);// 上锁
        printf("线程 [%ld] ⽣产⼀个产品,产品数量为:%dn",pthread_self(),++number);
        pthread_mutex_unlock(&mtx);// 解锁

        //! 唤醒cond阻塞的线程
        //! @param cond 条件变量
        //pthread_cond_signal(&cond);//! 只能唤醒一个线程,如果有多个线程在等待,则只有一个线程会被唤醒
        //唤醒所有线程
        pthread_cond_broadcast(&cond);
    }
    pthread_exit((void *)0);// 线程退出
}



int main(int argc,char *argv[]) {

    pthread_t tid[argc-1];// 线程ID
    int i;
    int err;
    int total_of_produce = 0;// 总共生产的产品数量
    int total_of_consume = 0;// 总共消费的产品数量
    bool done = false;// 是否完成生产
    //循环创建线程
    for (i = 1;i < argc;i++){
        total_of_produce += atoi(argv[i]);// 计算总共需要生产的产品数量
        // 创建线程
        err = pthread_create(&tid[i-1],NULL,thread_handler,(void *)argv[i]);
        if (err != 0){
            perror("[ERROR] pthread_create(): ");
            exit(EXIT_FAILURE);
        }
    }
    //消费者
    for (;;){
        //*先获取锁,再进行条件变量的等待
        pthread_mutex_lock(&mtx);// 上锁

        //*while循环来判断条件,避免虚假唤醒
        while(number == 0) {// 等待生产者生产产品
            //! 等待条件变量
            //! @param cond 条件变量
            //! @param mtx 互斥锁
            //! 函数中会释放互斥锁,并阻塞线程,
            //! 直到条件变量被唤醒,再重新竞争互斥锁,获取互斥锁并继续执行
            pthread_cond_wait(&cond, &mtx);
        }
        while(number > 0){
            total_of_consume++;// 总共消费的产品数量
            printf("消费⼀个产品,产品数量为:%dn",--number);// 消费产品
            done = total_of_consume >= total_of_produce;// 是否完成生产
        }
        pthread_mutex_unlock(&mtx);// 解锁

        if (done)// 是否完成生产
            break;
    }

    // 等待线程退出
    for(i = 0;i < argc-1;i++){
        pthread_join(tid[i],NULL);
    }

    return 0;

}
    //循环创建线程
    for (i = 1;i < argc;i++){
        total_of_produce += atoi(argv[i]);// 计算总共需要生产的产品数量
        // 创建线程
        err = pthread_create(&tid[i-1],NULL,thread_handler,(void *)argv[i]);
        if (err != 0){
            perror("[ERROR] pthread_create(): ");
            exit(EXIT_FAILURE);
        }
    }
    //消费者
    for (;;){
        //*先获取锁,再进行条件变量的等待
        pthread_mutex_lock(&mtx);// 上锁

        //*while循环来判断条件,避免虚假唤醒
        while(number == 0) {// 等待生产者生产产品
            //! 等待条件变量
            //! @param cond 条件变量
            //! @param mtx 互斥锁
            //! 函数中会释放互斥锁,并阻塞线程,
            //! 直到条件变量被唤醒,再重新竞争互斥锁,获取互斥锁并继续执行
            pthread_cond_wait(&cond, &mtx);
        }
        while(number > 0){
            total_of_consume++;// 总共消费的产品数量
            printf("消费⼀个产品,产品数量为:%dn",--number);// 消费产品
            done = total_of_consume >= total_of_produce;// 是否完成生产
        }
        pthread_mutex_unlock(&mtx);// 解锁

        if (done)// 是否完成生产
            break;
    }

    // 等待线程退出
    for(i = 0;i < argc-1;i++){
        pthread_join(tid[i],NULL);
    }

    return 0;

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127