Partage de technologie

Linux multi-processus et multi-threading (8) Multi-threading

2024-07-12

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

Multithreading

Définition du fil

Un thread est une unité d'exécution dans un processus.

Responsable de l'exécution des programmes dans le processus en cours,

Il y a au moins un thread dans un processus

Il peut y avoir plusieurs threads dans un processus

Plusieurs threads partagent toutes les ressources du même processus et chaque thread participe à la planification unifiée du système d'exploitation

Cela peut être simplement compris comme processus = ressources mémoire + thread principal + thread enfant +…

Fils et processus

Pour les tâches étroitement liées, le multithread est préféré pendant la simultanéité. Pour les tâches qui ne sont pas étroitement liées et relativement indépendantes, il est recommandé de choisir le multi-processus ;

  • Processus : unité de base d'allocation de ressources par le système d'exploitation. Il s'agit de la plus petite unité d'allocation de ressources, de l'unité d'exécution et de planification du programme et de l'instance en cours d'exécution du programme.
  • Thread : Il s'agit de l'unité de base de planification et de répartition du processeur, de la plus petite unité d'exécution du processeur, de la plus petite unité de flux d'exécution de programme et de la plus petite unité d'exécution de programme.

La différence entre les threads et les processus :

  • espace mémoire
    • Plusieurs threads dans un processus partagent le même espace mémoire
    • Plusieurs processus ont un espace mémoire indépendant
  • Communication entre processus/threads
    • Communication simple entre les threads
    • La communication inter-processus est complexe

Ressources de discussion

  • Ressources de processus partagées
    • même espace d'adressage
    • tableau des descripteurs de fichiers
    • Comment chaque signal est traité
    • répertoire de travail actuel
    • identifiant utilisateur et identifiant de groupe
  • Des ressources uniques
    • Pile de discussions
    • Chaque fil de discussion contient des informations de contexte privées
    • Identifiant du sujet
    • Valeur du registre
    • valeur du numéro d'erreur
    • Mots de masque de signalisation et priorités de planification

Commandes liées au fil de discussion

Il existe de nombreuses commandes pour afficher le processus dans le système Linux, notamment pidstat, top, ps, vous pouvez afficher le processus, vous pouvez également afficher un
fil en cours de traitement

commande pidstat

Une fois l'outil sysstat installé sous Ubuntu, pidstat peut être pris en charge.

sudo apt install sysstat

Possibilités

-t : Afficher les threads associés au processus spécifié

-p : spécifie le pid du processus

Exemple

Afficher les threads associés au processus 12345

sudo pidstat -t -p 12345

Afficher les threads associés à tous les processus

sudo pidstat -t

Afficher les threads associés au processus 12345, sortie toutes les 1 seconde

sudo pidstat -t -p 12345 1

Afficher les threads associés à tous les processus, sortie toutes les 1 seconde

sudo pidstat -t 1

commande supérieure

Utilisez la commande top pour afficher les threads sous un certain processus. Vous devez utiliser l'option -H en combinaison avec -p pour spécifier le pid.

Possibilités

-H : Afficher les informations sur le fil de discussion

-p : spécifie le pid du processus

Exemple

Afficher les threads associés au processus 12345

sudo top -H -p 12345

Afficher les threads associés à tous les processus

sudo top -H

commande ps

Utilisez la commande ps combinée avec l'option -T pour afficher tous les threads d'un processus

Possibilités

-T : Afficher les informations sur le fil de discussion

-p : spécifie le pid du processus

Exemple

Afficher les threads associés au processus 12345

sudo ps -T -p 12345

Afficher les threads associés à tous les processus

sudo ps -T

Scénarios de concurrence courants

1. Mode multi-processus

En mode multi-processus, chaque processus est responsable de différentes tâches sans interférer les uns avec les autres. Chaque processus s'exécute dans un espace mémoire différent sans s'influencer mutuellement.

  • avantage:
    • L'espace d'adressage du processus est indépendant. Une fois qu'une exception se produit dans un processus, elle n'affectera pas les autres processus.
  • défaut:
    • Chaque processus doit allouer de l'espace mémoire indépendant. La création d'un processus coûte cher et prend plus de mémoire.
    • La collaboration inter-processus et la communication inter-processus sont plus complexes
  • Scène applicable :
    • Plusieurs tâches ne sont pas très étroitement liées, le mode multi-processus peut donc être utilisé
    • Il n'y a aucune dépendance entre les tâches et le mode multi-processus peut être utilisé

2. Mode multithread

En mode multithread, il peut y avoir plusieurs threads dans un processus, partageant le même espace mémoire, et les threads peuvent communiquer directement.

  • avantage:
    • La communication entre les threads est simple
    • Plusieurs threads du même processus peuvent partager des ressources et améliorer l'utilisation des ressources.
  • défaut:
    • Les threads n'ont pas d'espaces d'adressage de processus indépendants. Une fois le thread principal terminé, les autres threads se fermeront également.
    • La commutation et la planification des threads nécessitent des ressources. Un trop grand nombre de threads consommera des ressources système.
    • La synchronisation entre les threads est complexe et les problèmes de sécurité des threads doivent être pris en compte
  • Scène applicable :
    • Il existe des dépendances entre les tâches et le mode multi-thread peut être utilisé
    • La communication entre tâches est relativement fréquente et le mode multi-thread peut être utilisé.

Créer un fil de discussion

1. pthread_create()

pthread_create() est utilisé pour créer un thread une fois la création réussie, le thread commence à s'exécuter.
Une fois pthread_create() appelé avec succès, il renverra 0, sinon il renverra un code d'erreur.

Fichier d'en-tête de fonction :

#include <pthread.h>

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

Description du paramètre :

  • thread : pointeur vers le type pthread_t, utilisé pour stocker l’ID du thread.
  • attr : attribut de thread, qui peut être NULL, indiquant que l'attribut par défaut est utilisé.
  • start_routine : Fonction d'entrée du thread.
  • arg : paramètres transmis à la fonction d'entrée de thread.

valeur de retour :

  • 0 : créé avec succès.
  • EAGAIN : ressources insuffisantes, la création du thread a échoué.
  • EINVAL : le paramètre n'est pas valide.
  • ENOMEM : Mémoire insuffisante, la création du thread a échoué.

Avis:

  • Une fois que le thread enfant est créé avec succès, son exécution sera planifiée indépendamment et exécutée simultanément avec d’autres threads.
  • La bibliothèque -lpthread doit être liée lors de la compilation.

Exemple : Créer un fil de discussion

// 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() quitte le thread

pthread_exit() est utilisé pour quitter le thread. Une fois l'exécution du thread terminée, pthread_exit() sera automatiquement appelé pour quitter.

Fichier d'en-tête de fonction :

#include <pthread.h>

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

Description du paramètre :

  • retval : la valeur renvoyée à la fin du thread.
  • Une fois la fonction thread exécutée, pthread_exit() sera automatiquement appelé pour quitter.

3. pthread_join() attend la fin du thread

pthread_join() est utilisé pour attendre la fin du thread,
Après avoir appelé pthread_join(), le thread actuel sera bloqué jusqu'à la fin du thread.

Fichier d'en-tête de fonction :

#include <pthread.h>

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

Description du paramètre :

  • fil de discussion : ID du fil de discussion.
  • retval : pointeur vers la valeur de retour du thread, utilisé pour stocker la valeur renvoyée à la sortie du thread. (pointeur secondaire)

valeur de retour :

  • 0 : Attendez le succès.
  • EINVAL : le paramètre n'est pas valide.
  • ESRCH : l'ID de thread n'existe pas.
  • EDEADLK : le thread est dans un état de blocage.

Exemple:

// 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

Séparation des fils

Les fils sont divisés en pouvant être combinés et détachables

  • Peut être combiné
    • Un thread combinable peut récupérer ses ressources et être tué par d'autres threads ; ses ressources mémoire (telles que la pile) ne sont pas libérées tant qu'elles n'ont pas été récupérées par d'autres threads.
    • L'état par défaut de création du thread est combinable. La fonction pthread_join peut être appelée par d'autres threads pour attendre que le thread enfant se termine et libère les ressources associées.
  • séparable
    • Il ne peut pas être recyclé ou supprimé par d'autres threads. Les ressources du thread sont libérées par le système lorsqu'il se termine.
// 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

Créer plusieurs fils de discussion

Exemple 1 : créer plusieurs threads pour effectuer différentes tâches

// 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

Exemple 2 : créer plusieurs threads pour effectuer la même tâche

// 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

Communication entre les threads

Les autres communications entre processus s'appliquent également à la communication entre les threads.

Le thread principal transmet les paramètres au thread enfant

Lors de la création d'un thread enfant via la fonction pthread_create(), le quatrième paramètre de pthread_create() est le paramètre passé à la fonction du thread enfant.

Le sous-thread transmet les paramètres au thread principal

Lorsque vous quittez le thread enfant via la fonction pthread_exit(), vous pouvez transmettre des paramètres au thread principal.

void pth_exit(void *retval);
  • 1

En attendant la fin du sous-thread via la fonction pthread_join(), obtenez les paramètres de retour du sous-thread.

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

Exemple:

// 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 du fil de discussion

Mutex du fil de discussion

Mutex est un mécanisme de synchronisation utilisé pour contrôler l'accès aux ressources partagées.

Le principal avantage des threads est la possibilité de partager des informations via des variables globales, mais ce partage pratique a un coût :

Il faut s'assurer que plusieurs threads ne modifient pas la même variable en même temps

Un certain thread ne lira pas les variables modifiées par d'autres threads. En fait, deux threads ne peuvent pas accéder à la section critique en même temps.

Le principe du verrouillage mutex

Le principe d'un verrouillage mutex est que lorsqu'un thread tente d'entrer dans une zone mutex, si la zone mutex est déjà occupée par d'autres threads, le thread sera bloqué jusqu'à ce que la zone mutex soit libérée.

Il s'agit essentiellement d'une variable de type pthread_mutex_t, qui contient une valeur entière pour représenter l'état de la zone mutex.
Lorsque la valeur est 1, cela signifie que la ressource critique actuelle peut entrer en compétition pour l'accès et que le thread qui obtient le verrou mutex peut accéder à la section critique. À ce stade, la valeur est 0 et les autres threads ne peuvent qu'attendre.
Lorsque la valeur est 0, cela signifie que la ressource critique actuelle est occupée par d'autres threads et ne peut pas entrer dans la section critique et ne peut qu'attendre.

Caractéristiques des verrous 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
  • Le verrou mutex est une variable de type pthread_mutex_t, qui représente un verrou mutex.
  • Si deux threads accèdent à la même variable pthread_mutex_t, alors ils accèdent au même verrou mutex
  • Les variables correspondantes sont définies dans le fichier d'en-tête pthreadtypes.h, qui est une union contenant une structure.

Utilisation de verrous mutex

Il existe deux manières principales d'initialiser les verrous mutex de thread :

initialisation statique

  • Définissez une variable de type pthread_mutex_t et initialisez-la à PTHREAD_MUTEX_INITIALIZER.
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER
  • 1

initialisation dynamique

Initialisation dynamique L'initialisation dynamique implique principalement deux fonctions : la fonction pthread_mutex_init et la fonction pthread_mutex_destroy

Fonction pthread_mutex_init()

Utilisé pour initialiser un verrou mutex, il accepte deux paramètres : l'adresse du verrou mutex et les attributs du verrou mutex.

Fichier d'en-tête de fonction :

#include <pthread.h>

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

Description du paramètre :

  • mutex : Pointeur vers le type pthread_mutex_t, utilisé pour stocker l'adresse du verrou mutex.
  • attr : l'attribut du verrou mutex, qui peut être NULL, indiquant que l'attribut par défaut est utilisé.

valeur de retour :

  • 0 : Initialisation réussie.
  • Un code d'erreur est renvoyé en cas d'échec.
Fonction pthread_mutex_destroy()

Utilisé pour détruire le verrou mutex, il accepte un paramètre : l'adresse du verrou mutex.

Fichier d'en-tête de fonction :

#include <pthread.h>

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

Description du paramètre :

  • mutex : Pointeur vers le type pthread_mutex_t, utilisé pour stocker l'adresse du verrou mutex.

valeur de retour :

  • 0 : Destruction réussie.
  • Un code d'erreur est renvoyé en cas d'échec.

Exemple:

// 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

Synchronisation des threads

Synchronisation des threads : fait référence à l'accès ordonné aux ressources par les visiteurs via d'autres mécanismes sur la base de l'exclusion mutuelle (dans la plupart des cas).

Variable de condition : mécanisme fourni par la bibliothèque de threads spécifiquement pour la synchronisation des threads.

Un scénario d’application typique pour la synchronisation des threads se situe entre producteurs et consommateurs.

Problèmes de producteurs et de consommateurs

Dans ce modèle, il est divisé en thread producteur et thread consommateur, et le processus de synchronisation de plusieurs threads est simulé via ce thread.

Dans ce modèle, les composants suivants sont requis :

  • Entrepôt : utilisé pour stocker des produits, généralement en tant que ressource partagée
  • Fil de production : utilisé pour fabriquer des produits
  • Fil de consommation : utilisé pour consommer des produits

principe:

Lorsqu'il n'y a pas de produit dans l'entrepôt, le thread consommateur doit attendre qu'il y ait un produit avant de pouvoir le consommer.

Lorsque l'entrepôt est rempli de produits, le thread producteur doit attendre que le thread consommateur consomme les produits.

Exemple d'implémentation de modèles de producteur et de consommateur basés sur des verrous mutex

Le fil conducteur est le consommateur

n sous-threads en tant que producteurs

// 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

variable de condition

Une variable de condition est un mécanisme de synchronisation qui permet à un thread d'attendre qu'une certaine condition soit remplie avant de continuer à s'exécuter.

Le principe d'une variable de condition est qu'elle contient un verrou mutex et une file d'attente.

Les verrous mutex sont utilisés pour protéger les files d'attente et les variables de condition.

Insérer la description de l'image ici

Initialisation des variables de condition

La nature de la variable de condition est de type 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

L'initialisation des variables de condition est divisée en initialisation statique et initialisation dynamique.

initialisation statique

Pour les variables de condition initialisées statiquement, vous devez d'abord définir une variable de type pthread_cond_t, puis l'initialiser sur PTHREAD_COND_INITIALIZER.

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
  • 1

Initialisation dynamique pthread_cond_init()

Pour initialiser dynamiquement une variable de condition, vous devez d'abord définir une variable de type pthread_cond_t, puis appeler la fonction pthread_cond_init pour l'initialiser.

Fichier d'en-tête de fonction :

#include <pthread.h>

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

Description du paramètre :

  • cond : Pointeur vers le type pthread_cond_t, utilisé pour stocker l'adresse de la variable de condition.
  • attr : attribut de la variable de condition, qui peut être NULL pour utiliser l'attribut par défaut.

valeur de retour :

  • 0 : Initialisation réussie.
  • Un code d'erreur est renvoyé en cas d'échec.

pthread_cond_destroy()

Utilisé pour détruire des variables de condition, il accepte un paramètre : l'adresse de la variable de condition.

Fichier d'en-tête de fonction :

#include <pthread.h>

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

Description du paramètre :

  • cond : Pointeur vers le type pthread_cond_t, utilisé pour stocker l'adresse de la variable de condition.

valeur de retour :

  • 0 : Destruction réussie.
  • Un code d'erreur est renvoyé en cas d'échec.

Utilisation de variables de condition

L'utilisation des variables de condition est divisée en attente et notification

Attendez pthread_cond_wait()

La fonction d'attente pthread_cond_wait() accepte trois paramètres : l'adresse de la variable de condition, l'adresse du verrou mutex et le temps d'attente.

Fichier d'en-tête de fonction :

#include <pthread.h>

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

Description du paramètre :

  • cond : Pointeur vers le type pthread_cond_t, utilisé pour stocker l'adresse de la variable de condition.
  • mutex : Pointeur vers le type pthread_mutex_t, utilisé pour stocker l'adresse du verrou mutex.
  • abstime : timeout, qui peut être NULL, indiquant qu'il n'y a pas de timeout.

valeur de retour :

  • 0 : Attendez le succès.
  • Un code d'erreur est renvoyé en cas d'échec.

Notifier pthread_cond_signal()

fonction de notification
pthread_cond_signal() accepte un paramètre : l'adresse de la variable de condition.

Fichier d'en-tête de fonction :

#include <pthread.h>

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

Description du paramètre :

  • cond : Pointeur vers le type pthread_cond_t, utilisé pour stocker l'adresse de la variable de condition.

valeur de retour :

  • 0 : notification réussie.
  • Un code d'erreur est renvoyé en cas d'échec.

Notifier tous les pthread_cond_broadcast()

Notifier toutes les fonctions
pthread_cond_broadcast() accepte un paramètre : l'adresse de la variable de condition.

Fichier d'en-tête de fonction :

#include <pthread.h>

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

Description du paramètre :

  • cond : Pointeur vers le type pthread_cond_t, utilisé pour stocker l'adresse de la variable de condition.

valeur de retour :

  • 0 : notification réussie.
  • Un code d'erreur est renvoyé en cas d'échec.

Exemple de mise en œuvre de modèles de producteur et de consommateur basés sur des variables conditionnelles

Insérer la description de l'image ici

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

Pourquoi les variables de condition doivent-elles être utilisées conjointement avec des verrous mutex ?

Protégez les données partagées :

Les verrous mutex sont utilisés pour protéger les données partagées et garantir qu'un seul thread peut accéder et modifier les données en même temps.

Cela évite les courses de données et les incohérences.

Les variables de condition sont utilisées pour la communication entre les threads afin d'informer les autres threads qu'une certaine condition a été remplie.

Cependant, le fonctionnement des variables de condition ne fournit pas en lui-même une protection pour les données partagées, elles doivent donc être utilisées conjointement avec un verrou mutex.

Évitez les faux réveils :

Une caractéristique des variables de condition est qu’un réveil intempestif peut se produire.

Autrement dit, le fil de discussion est réveillé sans notification explicite. Afin d'éviter des opérations incorrectes causées par cette situation,

Le thread doit revérifier si la condition est réellement remplie après le réveil.

L'utilisation d'un mutex garantit que les données partagées ne sont pas modifiées par d'autres threads lors de la vérification des conditions, évitant ainsi les erreurs causées par des réveils parasites.

Assurez-vous que la notification est correcte :

Lorsqu'un thread notifie d'autres threads via des variables de condition, il doit s'assurer que les données partagées ont été mises à jour avant de notifier.

Un mutex garantit cela, en garantissant que toutes les opérations de mise à jour des données sont terminées avant de libérer le verrou.

De même, le thread recevant la notification doit conserver le mutex avant de vérifier la condition pour garantir que les données sont stables lorsque la condition est vérifiée.

Implémentez des modèles de synchronisation complexes :
La combinaison de verrous mutex avec des variables de condition peut implémenter des modèles de synchronisation plus complexes, tels que des problèmes producteur-consommateur, des problèmes lecteur-écrivain, etc. Les mutex protègent les données partagées et les variables de condition sont utilisées pour la coordination et la communication entre les 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