Comment les barrières peuvent-elles être détruites dès le retour de pthread_barrier_wait?

Cette question est basée sur:

Quand est-il prudent de détruire une barrière de lecture?

et le rapport de bogue glibc récent:

http://sourceware.org/bugzilla/show_bug.cgi?id=12674

Je ne suis pas sûr de la question des sémaphores rapscope dans la glibc, mais je suppose que cela est supposé valide pour détruire une barrière dès que pthread_barrier_wait retour, comme indiqué dans la question liée ci-dessus. (Normalement, le thread qui a obtenu PTHREAD_BARRIER_SERIAL_THREAD , ou un thread “spécial” qui se considérait déjà comme “responsable” de l’object barrière, serait celui qui le détruirait.) Le principal cas d’utilisation auquel je peux penser est lorsqu’un barrière est utilisé pour synchroniser l’utilisation des données par un nouveau thread sur la stack du thread en création, afin d’éviter que le thread en création ne retourne jusqu’à ce que le nouveau thread puisse utiliser les données; d’autres barrières ont probablement une durée de vie égale à celle de l’ensemble du programme ou contrôlées par un autre object de synchronisation.

Dans tous les cas, comment une implémentation peut-elle garantir que la destruction de la barrière (et éventuellement le mappage de la mémoire dans laquelle elle réside) est sécurisée dès que pthread_barrier_wait revient dans un thread? Il semble que les autres threads qui ne sont pas encore retournés devraient examiner au moins une partie de l’object obstacle pour terminer leur travail et le renvoyer, un peu comme comment, dans le rapport de bogue glibc cité ci-dessus, sem_post doit examiner le nombre de serveurs après avoir ajusté la valeur du sémaphore.

Je vais aborder un autre problème avec un exemple d’implémentation de pthread_barrier_wait() qui utilise la fonctionnalité mutex et la variable de condition, comme cela pourrait être fourni par une implémentation de pthreads. Notez que cet exemple n’essaie pas de prendre en compte les considérations de performances (en particulier, lorsque les threads en attente sont débloqués, ils sont tous resérialisés lors de la fermeture de l’attente). Je pense qu’utiliser quelque chose comme les objects Linux Futex pourrait aider à résoudre les problèmes de performances, mais les Futex sont encore en grande partie hors de mon expérience.

De plus, je doute que cet exemple gère correctement les signaux ou les erreurs (voire pas du tout dans le cas de signaux). Mais je pense qu’un support adéquat pour ces choses peut être ajouté comme exercice pour le lecteur.

Ma principale crainte est que l’exemple présente une situation de concurrence critique ou une impasse (la gestion du mutex est plus complexe que je ne le souhaite). Notez également qu’il s’agit d’un exemple qui n’a même pas été compilé. Traitez-le comme un pseudo-code. N’oubliez pas non plus que mon expérience concerne principalement Windows. J’aborde ce problème plus que comme une opportunité éducative. La qualité du pseudo-code risque donc d’être assez faible.

Cependant, mis à part les disclaimers, je pense que cela peut donner une idée de la manière dont le problème posé dans la question pourrait être traité (c’est-à-dire, comment la fonction pthread_barrier_wait() peut-elle détruire l’object pthread_barrier_t qu’elle utilise sans danger d’utiliser l’object barrière par un ou plusieurs fils lorsqu’ils sortent).

Voici:

 /* * Since this is a part of the implementation of the pthread API, it uses * reserved names that start with "__" for internal structures and functions * * Functions such as __mutex_lock() and __cond_wait() perform the same function * as the corresponding pthread API. */ // struct __barrier_wait data is intended to hold all the data // that `pthread_barrier_wait()` will need after releasing // waiting threads. This will allow the function to avoid // touching the passed in pthread_barrier_t object after // the wait is satisfied (since any of the released threads // can destroy it) struct __barrier_waitdata { struct __mutex cond_mutex; struct __cond cond; unsigned waiter_count; int wait_complete; }; struct __barrier { unsigned count; struct __mutex waitdata_mutex; struct __barrier_waitdata* pwaitdata; }; typedef struct __barrier pthread_barrier_t; int __barrier_waitdata_init( struct __barrier_waitdata* pwaitdata) { waitdata.waiter_count = 0; waitdata.wait_complete = 0; rc = __mutex_init( &waitdata.cond_mutex, NULL); if (!rc) { return rc; } rc = __cond_init( &waitdata.cond, NULL); if (!rc) { __mutex_destroy( &pwaitdata->waitdata_mutex); return rc; } return 0; } int pthread_barrier_init(pthread_barrier_t *barrier, const pthread_barrierattr_t *attr, unsigned int count) { int rc; result = __mutex_init( &barrier->waitdata_mutex, NULL); if (!rc) return result; barrier->pwaitdata = NULL; barrier->count = count; //TODO: deal with attr } int pthread_barrier_wait(pthread_barrier_t *barrier) { int rc; struct __barrier_waitdata* pwaitdata; unsigned target_count; // potential waitdata block (only one thread's will actually be used) struct __barrier_waitdata waitdata; // nothing to do if we only need to wait for one thread... if (barrier->count == 1) return PTHREAD_BARRIER_SERIAL_THREAD; rc = __mutex_lock( &barrier->waitdata_mutex); if (!rc) return rc; if (!barrier->pwaitdata) { // no other thread has claimed the waitdata block yet - // we'll use this thread's rc = __barrier_waitdata_init( &waitdata); if (!rc) { __mutex_unlock( &barrier->waitdata_mutex); return rc; } barrier->pwaitdata = &waitdata; } pwaitdata = barrier->pwaitdata; target_count = barrier->count; // all data necessary for handling the return from a wait is pointed to // by `pwaitdata`, and `pwaitdata` points to a block of data on the stack of // one of the waiting threads. We have to make sure that the thread that owns // that block waits until all others have finished with the information // pointed to by `pwaitdata` before it returns. However, after the 'big' wait // is completed, the `pthread_barrier_t` object that's passed into this // function isn't used. The last operation done to `*barrier` is to set // `barrier->pwaitdata = NULL` to satisfy the requirement that this function // leaves `*barrier` in a state as if `pthread_barrier_init()` had been called - and // that operation is done by the thread that signals the wait condition // completion before the completion is signaled. // note: we're still holding `barrier->waitdata_mutex`; rc = __mutex_lock( &pwaitdata->cond_mutex); pwaitdata->waiter_count += 1; if (pwaitdata->waiter_count < target_count) { // need to wait for other threads __mutex_unlock( &barrier->waitdata_mutex); do { // TODO: handle the return code from `__cond_wait()` to break out of this // if a signal makes that necessary __cond_wait( &pwaitdata->cond, &pwaitdata->cond_mutex); } while (!pwaitdata->wait_complete); } else { // this thread satisfies the wait - unblock all the other waiters pwaitdata->wait_complete = 1; // 'release' our use of the passed in pthread_barrier_t object barrier->pwaitdata = NULL; // unlock the barrier's waitdata_mutex - the barrier is // ready for use by another set of threads __mutex_unlock( barrier->waitdata_mutex); // finally, unblock the waiting threads __cond_broadcast( &pwaitdata->cond); } // at this point, barrier->waitdata_mutex is unlocked, the // barrier->pwaitdata pointer has been cleared, and no further // use of `*barrier` is permitted... // however, each thread still has a valid `pwaitdata` pointer - the // thread that owns that block needs to wait until all others have // dropped the pwaitdata->waiter_count // also, at this point the `pwaitdata->cond_mutex` is locked, so // we're in a critical section rc = 0; pwaitdata->waiter_count--; if (pwaitdata == &waitdata) { // this thread owns the waitdata block - it needs to hang around until // all other threads are done // as a convenience, this thread will be the one that returns // PTHREAD_BARRIER_SERIAL_THREAD rc = PTHREAD_BARRIER_SERIAL_THREAD; while (pwaitdata->waiter_count!= 0) { __cond_wait( &pwaitdata->cond, &pwaitdata->cond_mutex); }; __mutex_unlock( &pwaitdata->cond_mutex); __cond_destroy( &pwaitdata->cond); __mutex_destroy( &pwaitdata_cond_mutex); } else if (pwaitdata->waiter_count == 0) { __cond_signal( &pwaitdata->cond); __mutex_unlock( &pwaitdata->cond_mutex); } return rc; } 

17 juillet 20111: Mise à jour en réponse à un commentaire / une question sur les obstacles partagés par les processus

J’ai complètement oublié la situation concernant les obstacles partagés entre les processus. Et comme vous l’avez dit, l’idée que j’ai exposée échouera horriblement dans ce cas. Je n’ai pas vraiment d’expérience avec l’utilisation de la mémoire partagée POSIX, aussi toutes les suggestions que je ferais devraient être tempérées avec scepticisme .

Pour résumer (à mon avantage, si personne d’autre):

Lorsque l’un des threads obtient le contrôle après le retour de pthread_barrier_wait() , l’object barrière doit être à l’état “init” (toutefois, la dernière pthread_barrier_init() sur cet object l’a définie). L’API implique également qu’une fois que l’un des threads est renvoyé, un ou plusieurs des problèmes suivants peuvent se produire:

  • un autre appel à pthread_barrier_wait() pour démarrer un nouveau cycle de synchronisation des threads
  • pthread_barrier_destroy() sur l’object barrière
  • la mémoire allouée pour l’object barrière peut être libérée ou non partagée si elle se trouve dans une région de mémoire partagée.

Cela signifie qu’avant que l’appel de pthread_barrier_wait() permette à aucun thread de revenir, il doit plus ou moins s’assurer que tous les threads en attente n’utilisent plus l’object barrière dans le contexte de cet appel. Ma première réponse a abordé ce problème en créant un ensemble «local» d’objects de synchronisation (un mutex et une variable de condition associée) en dehors de l’object barrière qui bloquerait tous les threads. Ces objects de synchronisation locaux ont été alloués sur la stack du thread qui a appelé pthread_barrier_wait() premier.

Je pense qu’il faudrait faire quelque chose de similaire pour les obstacles partagés. Cependant, dans ce cas, allouer simplement ces objects de synchronisation sur la stack d’un thread n’est pas adéquat (car les autres processus n’auraient aucun access). Pour une barrière partagée par processus, ces objects devraient être alloués dans la mémoire partagée par processus. Je pense que la technique que j’ai énumérée ci-dessus pourrait être appliquée de la même manière:

  • le waitdata_mutex qui contrôle l’allocation des variables de synchronisation locales (le bloc waitdata) serait déjà dans la mémoire partagée par processus du fait qu’il se trouve dans la structure de la barrière. Bien sûr, lorsque la barrière est définie sur THEAD_PROCESS_SHARED , cet atsortingbut doit également être appliqué à waitdata_mutex
  • Lorsque __barrier_waitdata_init() est appelée pour initialiser la variable locale mutex & condition, il devra allouer ces objects en mémoire partagée au lieu d’utiliser simplement la variable waitdata basée sur la stack.
  • Lorsque le thread ‘cleanup’ détruit le mutex et la variable de condition du bloc waitdata , il doit également nettoyer l’allocation de mémoire partagée par processus pour le bloc.
  • dans le cas où la mémoire partagée est utilisée, il doit exister un mécanisme permettant de s’assurer que l’object de mémoire partagée est ouvert au moins une fois dans chaque processus et fermé le nombre correct de fois dans chaque processus (mais pas entièrement fermé avant chaque thread le processus est fini de l’utiliser). Je n’ai pas réfléchi à la façon dont cela serait fait …

Je pense que ces changements permettraient au système de fonctionner avec des obstacles partagés. le dernier point ci-dessus est un élément clé à comprendre. Une autre waitdata un nom pour l’object de mémoire partagée qui contiendra les données d’attente «locales» partagées par le waitdata . Il y a certains atsortingbuts que vous voudriez pour ce nom:

  • vous voudriez que la mémoire du nom réside dans la struct pthread_barrier_t afin que tous les processus y aient access; cela signifie une limite connue à la longueur du nom
  • vous voudriez que le nom soit unique pour chaque ‘instance’ d’un ensemble d’appels à pthread_barrier_wait() car il est possible qu’un deuxième tour soit lancé avant que tous les threads soient sortis du premier tour, en attente (de sorte que le bloc de mémoire partagé par processus configuré pour les données d’ waitdata peut-être pas encore été libéré). Ainsi, le nom doit probablement être basé sur des éléments tels que l’ID de processus, l’ID de thread, l’adresse de l’object barrière et un compteur atomique.
  • Je ne sais pas s’il y a des implications en termes de sécurité pour que le nom soit “devinable”. si tel est le cas, il faut append une certaine randomisation – aucune idée de combien. Peut-être aurez-vous également besoin de hacher les données mentionnées ci-dessus avec les bits aléatoires. Comme je l’ai dit, je ne sais vraiment pas si c’est important ou non.

Autant que je pthread_barrier_destroy n’est pas une opération immédiate. Vous pouvez le faire attendre jusqu’à ce que tous les threads qui sont encore dans leur phase de réveil soient réveillés.

Par exemple, vous pourriez avoir un awakening compteur atomique qui initialise le nombre de threads réveillés. Ensuite, il sera décrémenté comme dernière action avant le retour de pthread_barrier_wait . pthread_barrier_destroy pourrait alors tourner jusqu’à ce que le compteur tombe à 0 .