Question

Cette question est basée sur:

Quand est-il sûr de détruire une barrière pthread?

et le récent rapport de bogue de la glibc:

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

Je ne suis pas sûr du problème des sémaphores signalé dans la glibc, mais il est vraisemblablement censé être valide pour détruire une barrière dès que pthread_barrier_wait revient, comme indiqué ci-dessus. (Normalement, le thread qui obtient PTHREAD_BARRIER_SERIAL_THREAD, ou un thread "spécial" qui se considère déjà comme "responsable" de l'objet barrière, serait celui qui le détruit.) Le cas d'utilisation principal auquel je peux penser est quand une barrière est utilisée pour synchroniser l'utilisation des données d'un nouveau thread sur la pile du thread de création, empêchant le thread de création de revenir jusqu'à ce que le nouveau thread utilise 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 objet de synchronisation.

Dans tous les cas, comment une implémentation peut garantir que la destruction de la barrière (et peut-être même le démappage de la mémoire dans laquelle elle réside) est sûre dès que pthread_barrier_wait revient dans un thread? Il semble que les autres threads qui ne sont pas encore revenus auraient besoin d'examiner au moins une partie de l'objet barrière pour terminer leur travail et revenir, 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.

Était-ce utile?

La solution

Je vais prendre une autre fissure à ceci avec un exemple d'implémentation de pthread_barrier_wait() qui utilise la fonctionnalité mutex et variable de condition comme pourrait être fournie par une implémentation pthreads. Notez que cet exemple n'essaie pas de traiter des considérations de performances (en particulier, lorsque les threads en attente sont débloqués, ils sont tous re-sérialisés à la sortie de l'attente). Je pense que l'utilisation de quelque chose comme des objets Linux Futex pourrait aider à résoudre les problèmes de performances, mais les Futex sont encore à peu près 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 des signaux). Mais je pense qu'un support approprié pour ces choses peut être ajouté comme exercice pour le lecteur.

Ma principale crainte est que l'exemple puisse avoir une condition de concurrence ou une impasse (la gestion des mutex est plus complexe que je ne le souhaite). Notez également que c'est un exemple qui n'a même pas été compilé. Traitez-le comme un pseudo-code. Gardez également à l'esprit que mon expérience est principalement dans Windows - j'aborde cela plus comme une opportunité éducative qu'autre chose. La qualité du pseudo-code peut donc être assez faible.

Cependant, mis à part les clauses de non-responsabilité, je pense que cela peut donner une idée de la façon dont le problème posé dans la question pourrait être traité (c'est-à-dire comment la fonction pthread_barrier_wait() peut-elle permettre à l'objet pthread_barrier_t qu'il utilise d'être détruit par l'un des threads publiés sans danger d'utiliser l'objet barrière par un ou plusieurs threads en sortant).

Voilà:

/* 
 *  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 avec les barrières partagées entre les processus. Et comme vous l'avez mentionné, l'idée que j'ai décrite échouera horriblement dans ce cas. Je n'ai pas vraiment d'expérience avec l'utilisation de la mémoire partagée POSIX, donc toutes les suggestions que je fais doivent être tempérées avec scepticisme .

Pour résumer (pour mon bénéfice, si personne d'autre):

Quand l'un des threads obtient le contrôle après le retour de pthread_barrier_wait(), l'objet de barrière doit être dans l'état 'init' (cependant, le pthread_barrier_init() le plus récent sur cet objet l'a défini). L'API implique également qu'une fois l'un des threads retourné, une ou plusieurs des choses suivantes peuvent se produire:

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

Ces choses signifient qu'avant que l'appel pthread_barrier_wait() autorise n'importe quel thread à retourner, il doit pratiquement s'assurer que tous les threads en attente n'utilisent plus l'objet de 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'objets de synchronisation (un mutex et une variable de condition associée) en dehors de l'objet barrière qui bloquerait tous les threads. Ces objets de synchronisation locaux ont été alloués sur la pile du thread qui a appelé pthread_barrier_wait() en premier.

Je pense que quelque chose de similaire devrait être fait pour les obstacles partagés par le processus. Cependant, dans ce cas, allouer simplement ces objets de synchronisation sur la pile d'un thread n'est pas adéquat (puisque les autres processus n'auraient pas d'accès). Pour une barrière partagée par processus, ces objets 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 le processus parce qu'il se trouve dans la structure de barrière. Bien sûr, lorsque la barrière est définie sur THEAD_PROCESS_SHARED, cet attribut doit également être appliqué au waitdata_mutex
li>
  • lorsque __barrier_waitdata_init() est appelé pour initialiser la variable locale mutex & condition, il devrait allouer ces objets dans la mémoire partagée au lieu d'utiliser simplement la variable waitdata basée sur la pile.
  • lorsque le thread de «nettoyage» détruit le mutex et la variable de condition dans le bloc waitdata, il devra également nettoyer l'allocation de mémoire partagée par le processus pour le bloc.
  • dans le cas où la mémoire partagée est utilisée, il doit y avoir un mécanisme pour s'assurer que l'objet 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 complètement fermé avant chaque thread du processus a fini de l'utiliser). Je n'ai pas réfléchi exactement à la manière dont cela serait fait ...
  • Je pense que ces changements permettraient au système de fonctionner avec des barrières partagées par les processus. le dernier point ci-dessus est un élément clé à comprendre. Une autre consiste à construire un nom pour l'objet de mémoire partagée qui contiendra le waitdata partagé par le processus «local». Il y a certains attributs que vous voudriez pour ce nom:

    • vous voudriez que le stockage du nom réside dans la structure struct pthread_barrier_t afin que tous les processus y aient accès; 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 pourrait être possible pour un deuxième tour d'attente de commencer avant que tous les threads soient complètement sortis du premier tour en attente (le bloc de mémoire partagée par le processus configuré pour le waitdata n'a peut-être pas encore été libéré). Le nom doit donc probablement être basé sur des éléments tels que l'identifiant du processus, l'identifiant du thread, l'adresse de l'objet barrière et un compteur atomique.
    • Je ne sais pas si oui ou non il y a des implications de sécurité à avoir le nom "devinable". si c'est le cas, une certaine randomisation doit être ajoutée - aucune idée de combien. Vous devrez peut-être également 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.

    Autres conseils

    Pour autant que je sache, il n'est pas nécessaire que pthread_barrier_destroy soit une opération immédiate.Vous pourriez faire attendre que tous les threads qui sont encore en phase de réveil soient réveillés.

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

    Licencié sous: CC-BY-SA avec attribution
    Non affilié à StackOverflow
    scroll top