11 votes

Programmation POSIX pthread

Je dois coder un programme multithread (disons 2 threads) où chacun de ces threads effectue une tâche différente. De plus, ces threads doivent continuer à tourner infiniment en arrière-plan une fois lancés. Voici ce que j'ai fait. Quelqu'un peut-il me dire si la méthode est bonne et si vous voyez des problèmes ? De plus, j'aimerais savoir comment fermer les threads de manière systématique une fois que j'ai terminé l'exécution, par exemple avec Ctrl+C.

La fonction principale crée deux threads et les laisse tourner à l'infini comme ci-dessous.

Voici le squelette :

void    *func1();
void    *func2();

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

    pthread_t th1,th2;
    pthread_create(&th1, NULL, func1, NULL);
    pthread_create(&th2, NULL, func2, NULL);

    fflush (stdout);
    for(;;){
    }
    exit(0); //never reached
}

void *func1()
{
    while(1){
    //do something
    }
}

void *func2()
{
    while(1){
    //do something
    }
} 

Merci.

Code modifié en utilisant les entrées des réponses : Est-ce que je quitte les fils correctement ?

#include <stdlib.h>     /*  exit() */
#include <stdio.h>      /* standard in and output*/
#include <pthread.h>
#include <unistd.h>
#include <time.h>
#include <sys/time.h>
#include <sys/types.h>
#include <signal.h>
#include <semaphore.h>

sem_t end;

void    *func1();
void    *func2();

void ThreadTermHandler(int signo){
    if (signo == SIGINT) {
        printf("Ctrl+C detected !!! \n");
        sem_post(&end);
        }
}
void *func1()
{
    int value;
    for(;;){
        sem_getvalue(&end, &value);
        while(!value){
            printf("in thread 1 \n");
        }
    }
    return 0;
}

void *func2()
{
    int value;
    for(;;){
        sem_getvalue(&end, &value);
        while(!value){
            printf("value = %d\n", value);
        }
    }
    return 0;
}

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

    sem_init(&end, 0, 0);
    pthread_t th1,th2;
    int value  = -2;
    pthread_create(&th1, NULL, func1, NULL);
    pthread_create(&th2, NULL, func2, NULL);

    struct sigaction sa;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_SIGINFO;
    sa.sa_sigaction = ThreadTermHandler;
    // Establish a handler to catch CTRL+c and use it for exiting.
    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction for Thread Termination failed");
        exit( EXIT_FAILURE );
    }

    /* Wait for SIGINT. */
    while (sem_wait(&end)!=0){}
    //{
        printf("Terminating Threads.. \n");
        sem_post(&end);
                sem_getvalue(&end, &value);
        /* SIGINT received, cancel threads. */
        pthread_cancel(th1);
        pthread_cancel(th2);
        /* Join threads. */
        pthread_join(th1, NULL);
        pthread_join(th2, NULL);
    //}
    exit(0);
}

14voto

Alek Points 1565

Il existe principalement deux approches pour la terminaison des fils.

  • Utilisez un point d'annulation. Le fil se termine lorsqu'on lui demande d'annuler et il atteint un point d'annulation, ce qui met fin à l'exécution de manière contrôlée ;
  • Utilisez un signal. Demandez aux threads d'installer un gestionnaire de signal qui fournit un mécanisme de terminaison (en mettant un drapeau et en réagissant à EINTR ).

Les deux approches ont des limites. Se référer à Kill Thread dans la bibliothèque Pthread pour plus de détails.

Dans votre cas, il semble que ce soit une bonne occasion d'utiliser des points d'annulation. Je vais travailler avec un exemple commenté. Le contrôle d'erreur a été omis pour plus de clarté.

#define _POSIX_C_SOURCE 200809L
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void sigint(int signo) {
    (void)signo;
}

void *thread(void *argument) {
    (void)argument;
    for (;;) {
        // Do something useful.
        printf("Thread %u running.\n", *(unsigned int*)argument);

        // sleep() is a cancellation point in this example.
        sleep(1);
    }
    return NULL;
}

int main(void) {
    // Block the SIGINT signal. The threads will inherit the signal mask.
    // This will avoid them catching SIGINT instead of this thread.
    sigset_t sigset, oldset;
    sigemptyset(&sigset);
    sigaddset(&sigset, SIGINT);
    pthread_sigmask(SIG_BLOCK, &sigset, &oldset);

    // Spawn the two threads.
    pthread_t thread1, thread2;
    pthread_create(&thread1, NULL, thread, &(unsigned int){1});
    pthread_create(&thread2, NULL, thread, &(unsigned int){2});

    // Install the signal handler for SIGINT.
    struct sigaction s;
    s.sa_handler = sigint;
    sigemptyset(&s.sa_mask);
    s.sa_flags = 0;
    sigaction(SIGINT, &s, NULL);

    // Restore the old signal mask only for this thread.
    pthread_sigmask(SIG_SETMASK, &oldset, NULL);

    // Wait for SIGINT to arrive.
    pause();

    // Cancel both threads.
    pthread_cancel(thread1);
    pthread_cancel(thread2);

    // Join both threads.
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    // Done.
    puts("Terminated.");
    return EXIT_SUCCESS;
}

La nécessité de bloquer/débloquer les signaux est due au fait que si vous envoyez SIGINT au processus, n'importe quel thread peut l'attraper. Vous le faites avant de créer les threads pour éviter qu'ils le fassent eux-mêmes et qu'ils aient besoin de se synchroniser avec le parent. Une fois les threads créés, vous restaurez le masque et installez un gestionnaire.

Les points d'annulation peuvent être délicats si les threads allouent beaucoup de ressources ; dans ce cas, il faudra utiliser pthread_cleanup_push() y pthread_cleanup_pop() qui sont un désordre. Mais l'approche est réalisable et plutôt élégante si elle est utilisée correctement.

7voto

R.. Points 93718

La réponse dépend beaucoup de ce que vous voulez faire quand l'utilisateur appuie sur CtrlC .

Si vos threads de travail ne modifient pas de données qui doivent être sauvegardées à la sortie, vous n'avez rien à faire. L'action par défaut de SIGINT est de mettre fin à la processus et cela inclut tous les fils qui composent le processus.

Cependant, si vos threads doivent être nettoyés, vous avez du travail à faire. Il y a deux problèmes distincts que vous devez prendre en compte :

  • Comment traiter le signal et transmettre le message aux threads qu'ils doivent se terminer.
  • Comment vos threads reçoivent et traitent la demande de terminaison.

Tout d'abord, les gestionnaires de signaux sont une plaie. À moins que vous ne soyez très prudent, vous devez supposer que la plupart des fonctions de la bibliothèque ne sont pas légales pour être appelées à partir d'un gestionnaire de signaux. Heureusement, sem_post est spécifié pour être async-signal-safe, et peut répondre parfaitement à vos besoins :

  1. Au début de votre programme, initialiser un sémaphore avec sem_init(&exit_sem, 0, 0);
  2. Installer un gestionnaire de signaux pour SIGINT (et tout autre signal de terminaison que vous souhaitez gérer, comme par exemple SIGTERM ) qui effectue sem_post(&exit_sem); et les retours.
  3. Remplacer le for(;;); dans le fil principal avec while (sem_wait(&exit_sem)!=0) .
  4. Après sem_wait réussit, le thread principal doit informer tous les autres threads qu'ils doivent se retirer, puis attendre qu'ils se retirent tous.

Ce qui précède peut également être accompli sans sémaphores en utilisant des masques de signal et des sigwaitinfo mais je préfère l'approche du sémaphore parce qu'elle ne nécessite pas d'apprendre une sémantique compliquée des signaux.

Il y a plusieurs façons d'informer les fils de travail qu'il est temps d'arrêter. Je vois quelques options :

  1. Les faire vérifier sem_getvalue(&exit_sem) périodiquement et nettoie et quitte s'il retourne une valeur non nulle. Notez cependant que cela ne fonctionnera pas si le thread est bloqué indéfiniment, par exemple dans un appel à read o write .
  2. Utilice pthread_cancel et placer soigneusement les gestionnaires d'annulation ( pthread_cleanup_push ) partout.
  3. Utilice pthread_cancel mais aussi pthread_setcancelstate pour désactiver l'annulation pendant la majeure partie de votre code, et ne la réactiver que lorsque vous allez effectuer des opérations IO bloquantes. De cette façon, vous n'avez besoin de mettre les gestionnaires de nettoyage qu'aux endroits où l'annulation est activée.
  4. Apprenez la sémantique avancée des signaux, et configurez un gestionnaire de signaux et de signaux d'interruption supplémentaire que vous envoyez à tous les threads via pthread_kill ce qui provoquera le retour des appels syscalls bloquants avec un EINTR erreur. Ensuite, vos threads peuvent agir en fonction de cela et sortir de la manière normale du C via une chaîne de retours d'échec jusqu'à la fonction de départ.

Je ne recommanderais pas l'approche 4 pour les débutants Mais pour les programmeurs C avancés, c'est peut-être la meilleure solution, car elle permet d'utiliser l'idiome C existant, qui consiste à signaler les conditions exceptionnelles par des valeurs de retour plutôt que par des "exceptions".

Notez également qu'avec pthread_cancel vous devrez périodiquement appeler pthread_testcancel si vous n'appelez pas d'autres fonctions qui sont points d'annulation . Sinon, il ne sera jamais donné suite à la demande d'annulation.

3voto

SirDarius Points 13074

C'est une mauvaise idée :

for(;;){
}

car votre thread principal exécutera des instructions inutiles du CPU.

Si vous devez attendre dans le thread principal, utilisez pthread_join comme répondu dans cette question : Plusieurs threads dans un programme C

3voto

cnicutar Points 98451

Ce que vous avez fait fonctionne, je ne vois pas de problèmes évidents (sauf que vous ignorez la valeur de retour de la commande pthread_create ). Malheureusement, arrêter les fils est plus compliqué que vous ne le pensez . Le fait que vous vouliez utiliser des signaux est une autre complication. Voici ce que vous pourriez faire.

  • Dans les fils "enfants", utilisez pthread_sigmask pour bloquer les signaux
  • Dans le fil principal, utilisez sigsuspend d'attendre un signal
  • Une fois que vous avez reçu le signal, annulez ( pthread_cancel ) les fils d'enfants

Votre fil conducteur pourrait ressembler à quelque chose comme ceci :

/* Wait for SIGINT. */
sigsuspend(&mask);

/* SIGINT received, cancel threads. */
pthread_cancel(th1);
pthread_cancel(th2);

/* Join threads. */
pthread_join(th1, NULL);
pthread_join(th2, NULL);

De toute évidence, vous devez en savoir plus sur pthread_cancel et les points d'annulation. Vous pouvez également installer un gestionnaire de nettoyage . Et bien sûr, vérifiez chaque valeur de retour.

1voto

Maxim Yegorushkin Points 29380

J'ai regardé votre code mis à jour et il ne semble toujours pas correct.

Le traitement des signaux doit être effectué dans un seul thread. Les signaux destinés à un processus (comme SIGINT) sont délivrés à tout qui n'a pas ce signal bloqué. En d'autres termes, il n'y a aucune garantie qu'avec les trois threads que vous avez, ce sera le thread principal qui recevra le SIGINT. Dans les programmes multithreads, la meilleure pratique consiste à bloquer tous les signaux avant de créer des threads, et une fois que tous les threads ont été créés, à débloquer les signaux dans le thread principal uniquement (normalement, c'est le thread principal qui est le mieux placé pour gérer les signaux). Voir Concepts de signaux y Signalisation dans un processus multithread pour plus.

pthread_cancel est à éviter, il n'y a aucune raison de l'utiliser. Pour arrêter les threads, vous devez leur communiquer d'une manière ou d'une autre qu'ils doivent se terminer et attendre qu'ils se terminent volontairement. Normalement, les threads auront une sorte de boucle d'événements, il devrait donc être relativement simple d'envoyer un événement à l'autre thread.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X