2017-07-29 3 views
0

Cette question est très longue en raison des extraits de code et des explications détaillées. TL; DR, y a-t-il des problèmes avec les macros montrées ci-dessous, est-ce une solution raisonnable, et si non alors quelle est la manière la plus raisonnable de résoudre les problèmes présentés ci-dessous?Piratage de macros fiévreux pour gérer les problèmes liés aux annulations de threads et aux gestionnaires de nettoyage

J'écris actuellement une bibliothèque C qui traite les threads POSIX, et doit être capable de gérer proprement l'annulation de threads. En particulier, les fonctions de bibliothèque peuvent être appelées à partir de threads réglés pour être annulables (soit PTHREAD_CANCEL_DEFFERED ou PTHREAD_CANCEL_ASYNCHRONOUS canceltype) par l'utilisateur.

Actuellement les fonctions de la bibliothèque qui interfacent avec l'utilisateur commencent par un appel à pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldstate), et à chaque point de retour, je m'assure qu'un appel à pthread_setcancelstate(oldstate, &dummy) est fait pour restaurer tous les paramètres d'annulation que le thread avait précédemment. Cela empêche fondamentalement l'annulation du thread dans le code de la bibliothèque, garantissant ainsi que l'état global reste cohérent et que les ressources ont été correctement gérées avant de revenir.

Cette méthode a malheureusement quelques inconvénients:

  1. Il faut être sûr de restaurer la cancelstate à chaque point de retour. Cela rend la gestion difficile si la fonction a un flux de contrôle non trivial avec plusieurs points de retour. Oublier de le faire peut conduire à des threads qui ne sont pas annulés même après le retour de la bibliothèque. Nous avons seulement besoin d'empêcher les annulations aux points où les ressources sont allouées ou l'état global est incohérent. Une fonction de bibliothèque peut à son tour appeler d'autres fonctions de bibliothèque interne qui sont sans risque d'annulation, et idéalement, des annulations pourraient survenir à de tels points.

Voici un échantillon illustration des questions:

#include <stdlib.h> 
#include <unistd.h> 
#include <fcntl.h> 
#include <pthread.h> 

static void do_some_long_computation(char *buffer, size_t len) 
{ 
    (void)buffer; (void)len; 
    /* This is really, really long! */ 
} 

int mylib_function(size_t len) 
{ 
     char *buffer; 
     int oldstate, oldstate2; 

     pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldstate); 

     buffer = malloc(len); 

     if (buffer == NULL) { 
       pthread_setcancelstate(oldstate, &oldstate2); 
       return -1; 
     } 

     do_some_long_computation(buffer, len); 

     fd = open("results.txt", O_WRONLY); 

     if (fd < 0) { 
       free(buffer); 
       pthread_setcancelstate(oldstate, &oldstate2); 
       return -1; 
     } 

     write(fd, buffer, len); /* Normally also do error-check */ 
     close(fd); 

     free(buffer); 

     pthread_setcancelstate(oldstate, &oldstate2); 

     return 0; 
} 

Ici, il est pas si mal parce qu'il n'y a que 3 points de retour. On pourrait même restructurer le flux de contrôle de manière à forcer tous les chemins à atteindre un seul point de retour, peut-être avec le motif goto cleanup. Mais le deuxième problème n'est toujours pas résolu. Et imaginez avoir à le faire pour de nombreuses fonctions de la bibliothèque.

Le deuxième problème peut être résolu en enveloppant chaque allocation de ressources avec des appels à pthread_setcancelstate qui ne désactiveront que les annulations lors de l'allocation des ressources. Alors que les annulations sont désactivées, nous poussons également un gestionnaire de nettoyage (avec pthread_cleanup_push). On pourrait aussi déplacer toutes les allocations de ressources ensemble (ouvrir le fichier avant de faire le long calcul). Lors de la résolution du deuxième problème, il est encore difficile à gérer car chaque allocation de ressources doit être enveloppée dans ces appels pthread_setcancelstate et pthread_cleanup_[push|pop]. De plus, il n'est pas toujours possible de regrouper toutes les allocations de ressources, par exemple si elles dépendent des résultats du calcul. De plus, le flux de contrôle doit être changé car on ne peut pas retourner entre une paire pthread_cleanup_push et pthread_cleanup_pop (ce qui serait le cas si malloc renvoie NULL par exemple).

Afin de résoudre les deux problèmes, j'ai trouvé une autre méthode possible qui implique des hacks sales avec des macros.L'idée est de simuler quelque chose comme un bloc de section critique dans d'autres langages, pour insérer un bloc de code dans une portée «annulation-sûre».

C'est ce que le code de la bibliothèque ressemblerait (compiler avec -c -Wall -Wextra -pedantic):

#include <stdlib.h> 
#include <unistd.h> 
#include <fcntl.h> 
#include <pthread.h> 

#include "cancelsafe.h" 

static void do_some_long_computation(char *buffer, size_t len) 
{ 
    (void)buffer; (void)len; 
    /* This is really, really long! */ 
} 

static void free_wrapper(void *arg) 
{ 
     free(*(void **)arg); 
} 

static void close_wrapper(void *arg) 
{ 
     close(*(int *)arg); 
} 

int mylib_function(size_t len) 
{ 
     char *buffer; 
     int fd; 
     int rc; 

     rc = 0; 
     CANCELSAFE_INIT(); 

     CANCELSAFE_PUSH(free_wrapper, buffer) { 
       buffer = malloc(len); 

       if (buffer == NULL) { 
         rc = -1; 
         CANCELSAFE_BREAK(buffer); 
       } 
     } 

     do_some_long_computation(buffer, len); 

     CANCELSAFE_PUSH(close_wrapper, fd) { 
       fd = open("results.txt", O_WRONLY); 

       if (fd < 0) { 
         rc = -1; 
         CANCELSAFE_BREAK(fd); 
       } 
     } 

     write(fd, buffer, len); 

     CANCELSAFE_POP(fd, 1); /* close fd */ 
     CANCELSAFE_POP(buffer, 1); /* free buffer */ 

     CANCELSAFE_END(); 

     return rc; 
} 

Cela résout les deux problèmes dans une certaine mesure. Les paramètres cancelstate et les appels push/pop de nettoyage sont implicites dans les macros, de sorte que le programmeur doit uniquement spécifier les sections de code à annuler et les gestionnaires de nettoyage à appliquer. Le reste est fait dans les coulisses, et le compilateur s'assurera que chaque CANCELSAFE_PUSH est associé à un CANCELSAFE_POP.

La mise en œuvre des macros est la suivante:

#define CANCELSAFE_INIT() \ 
     do {\ 
       int CANCELSAFE_global_stop = 0 

#define CANCELSAFE_PUSH(cleanup, ident) \ 
       do {\ 
         int CANCELSAFE_oldstate_##ident, CANCELSAFE_oldstate2_##ident;\ 
         int CANCELSAFE_stop_##ident;\ 
         \ 
         if (CANCELSAFE_global_stop)\ 
           break;\ 
         \ 
         pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &CANCELSAFE_oldstate_##ident);\ 
         pthread_cleanup_push(cleanup, &ident);\ 
         for (CANCELSAFE_stop_##ident = 0; CANCELSAFE_stop_##ident == 0 && CANCELSAFE_global_stop == 0; CANCELSAFE_stop_##ident = 1, pthread_setcancelstate(CANCELSAFE_oldstate_##ident, &CANCELSAFE_oldstate2_##ident)) 

#define CANCELSAFE_BREAK(ident) \ 
           do {\ 
             CANCELSAFE_global_stop = 1;\ 
             pthread_setcancelstate(CANCELSAFE_oldstate_##ident, &CANCELSAFE_oldstate2_##ident);\ 
             goto CANCELSAFE_POP_LABEL_##ident;\ 
           } while (0) 

#define CANCELSAFE_POP(ident, execute) \ 
CANCELSAFE_POP_LABEL_##ident:\ 
         pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &CANCELSAFE_oldstate_##ident);\ 
         pthread_cleanup_pop(execute);\ 
         pthread_setcancelstate(CANCELSAFE_oldstate_##ident, &CANCELSAFE_oldstate2_##ident);\ 
       } while (0) 

#define CANCELSAFE_END() \ 
     } while (0) 

Cette combine plusieurs astuces macro que j'ai rencontrés auparavant.

Le motif do { } while (0) est utilisé pour avoir une macro multi-fonctions (avec point-virgule requis).

Les macros sont contraintes CANCELSAFE_PUSH et CANCELSAFE_POP à venir en paires par l'utilisation de la même astuce que le pthread_cleanup_push et pthread_cleanup_pop utilisant inégalés { et } accolades respectivement (ici est inégalée do { et } while (0) à la place). L'utilisation des boucles for est quelque peu inspirée par cette question. L'idée est que nous voulons appeler la pthread_setcancelstate fonction après le corps de la macro pour restaurer les annulations après le bloc CANCELSAFE_PUSH. J'utilise un drapeau d'arrêt qui est mis à 1 à la deuxième itération de la boucle.

L'ident est le nom de la variable qui sera publiée (ceci doit être un identifiant valide). Le cleanup_wrappers recevra son adresse , qui sera toujours valide dans une portée de gestionnaire de nettoyage selon ce answer. Ceci est fait parce que la valeur de la variable n'est pas encore initialisée au moment du nettoyage (et ne fonctionne pas non plus si la variable n'est pas de type pointeur). L'identifiant est également utilisé pour éviter les collisions de noms dans les variables temporaires et les étiquettes en les ajoutant en suffixe avec la macro de concaténation ##, en leur donnant des noms uniques.

La macro CANCELSAFE_BREAK est utilisée pour sauter hors du bloc cancelsafe et à droite dans le CANCELSAFE_POP_LABEL correspondant. Ceci est inspiré par le modèle goto cleanup, comme mentionné here. Il définit également le drapeau d'arrêt global.

L'arrêt global est utilisé pour éviter les cas où il peut y avoir deux paires PUSH/POP dans le même niveau d'étendue. Cela semble une situation improbable, mais si cela se produit, le contenu des macros est fondamentalement ignoré lorsque le drapeau d'arrêt global est mis à 1. Les macros CANCELSAFE_INIT et CANCELSAFE_END ne sont pas cruciales, elles évitent simplement la nécessité de déclarer l'arrêt global signaler nous-mêmes. Ceux-ci pourraient être sautés si le programmeur fait toujours toutes les poussées et ensuite toutes les pops consécutivement.

Après l'expansion des macros, on obtient le code suivant pour la mylib_function:

int mylib_function(size_t len) 
{ 
     char *buffer; 
     int fd; 
     int rc; 

     rc = 0; 
     do { 
       int CANCELSAFE_global_stop = 0; 

       do { 
         int CANCELSAFE_oldstate_buffer, CANCELSAFE_oldstate2_buffer; 
         int CANCELSAFE_stop_buffer; 

         if (CANCELSAFE_global_stop) 
           break; 

         pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &CANCELSAFE_oldstate_buffer); 
         pthread_cleanup_push(free_wrapper, &buffer); 
         for (CANCELSAFE_stop_buffer = 0; CANCELSAFE_stop_buffer == 0 && CANCELSAFE_global_stop == 0; CANCELSAFE_stop_buffer = 1, pthread_setcancelstate(CANCELSAFE_oldstate_buffer, &CANCELSAFE_oldstate2_buffer)) { 
           buffer = malloc(len); 

           if (buffer == NULL) { 
             rc = -1; 
             do { 
               CANCELSAFE_global_stop = 1; 
               pthread_setcancelstate(CANCELSAFE_oldstate_buffer, &CANCELSAFE_oldstate2_buffer); 
               goto CANCELSAFE_POP_LABEL_buffer; 
             } while (0); 
           } 
         } 

         do_some_long_computation(buffer, len); 

         do { 
           int CANCELSAFE_oldstate_fd, CANCELSAFE_oldstate2_fd; 
           int CANCELSAFE_stop_fd; 

           if (CANCELSAFE_global_stop) 
             break; 

           pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &CANCELSAFE_oldstate_fd); 
           pthread_cleanup_push(close_wrapper, &fd); 
           for (CANCELSAFE_stop_fd = 0; CANCELSAFE_stop_fd == 0 && CANCELSAFE_global_stop == 0; CANCELSAFE_stop_fd = 1, pthread_setcancelstate(CANCELSAFE_oldstate_fd, &CANCELSTATE_oldstate2_fd)) { 
             fd = open("results.txt", O_WRONLY); 

             if (fd < 0) { 
               rc = -1; 
               do { 
                 CANCELSAFE_global_stop = 1; 
                 pthread_setcancelstate(CANCELSAFE_oldstate_fd, &CANCELSAFE_oldstate2_fd); 
                 goto CANCELSAFE_POP_LABEL_fd; 
               } while (0); 
             } 
           } 

           write(fd, buffer, len); 

CANCELSAFE_POP_LABEL_fd: 
           pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &CANCELSAFE_oldstate_fd); 
           pthread_cleanup_pop(1); 
           pthread_setcancelstate(CANCELSAFE_oldstate_fd, &CANCELSAFE_oldstate2_fd); 
         } while (0); 

CANCELSAFE_POP_LABEL_buffer: 
         pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &CANCELSAFE_oldstate_buffer); 
         pthread_cleanup_pop(1); 
         pthread_setcancelstate(CANCELSAFE_oldstate_buffer, &CANCELSAFE_oldstate2_buffer); 
       } while (0); 
     } while (0); 

     return rc; 
} 

Maintenant, cet ensemble de macros est horrible à regarder et il est un peu difficile de comprendre comment ils fonctionnent exactement. D'un autre côté, il s'agit d'une tâche ponctuelle, et une fois écrites, elles peuvent être abandonnées et le reste du projet peut bénéficier de leurs avantages.

Je voudrais savoir s'il y a des problèmes avec les macros que j'ai pu ignorer, et s'il pourrait y avoir une meilleure façon d'implémenter des fonctionnalités similaires. De plus, selon vous, quelle solution serait la plus raisonnable? Y a-t-il d'autres idées qui pourraient mieux fonctionner pour résoudre ces problèmes (ou peut-être sont-elles réellement des problèmes)?

+0

Cela semble mieux faire suite à une révision de code, n'est-ce pas? Ou faites-vous face à quelque chose qui "ne fonctionne pas" *? – alk

+0

Je ne suis pas certain quel site stackexchange est le plus approprié pour cette question. Je serais heureux de le faire migrer partout où c'est approprié. –

+1

Personnellement, je n'aime pas les macros comme ça pour de nombreuses raisons. Dans ce cas. Il est beaucoup plus sûr d'utiliser les fonctions en ligne. Un peu plus d'écriture - beaucoup moins de débogage :). –

Répondre

0

Sauf si vous utilisez l'annulation asynchrone (ce qui est toujours très problématique), vous n'avez pas à désactiver l'annulation autour de malloc et free (et de nombreuses autres fonctions POSIX). L'annulation synchrone se produit uniquement aux points d'annulation, et ces fonctions ne le sont pas.

Vous utilisez abusivement les fonctions de gestion d'annulation POSIX pour implémenter un hook de sortie de portée. En général, si vous vous trouvez en train de faire des choses comme ça en C, vous devriez sérieusement envisager d'utiliser C++ à la place. Cela vous donnera une version beaucoup plus soignée de la fonctionnalité, avec une documentation abondante, et les programmeurs auront déjà l'expérience avec elle.