web-dev-qa-db-fra.com

Existe-t-il une mise en file d'attente sans verrouillage prête à la production ou une implémentation de hachage en C++

Je suis allé beaucoup chercher sur Google pour une file d'attente sans verrouillage en C++. J'ai trouvé du code et des essais - mais rien de ce que j'ai pu compiler. Un hachage sans verrou serait également le bienvenu.

RÉSUMÉ: Jusqu’à présent, je n’ai pas de réponse positive . Il n’existe pas de bibliothèque "prête pour la production" et, étonnamment, aucune des bibliothèques existantes ne respecte l’API des conteneurs STL. 

73
RED SOFT ADAIR

À partir de 1.53, boost fournit un ensemble de structures de données sans verrouillage , comprenant des files d'attente, des piles et des files d'attente mono-producteur/mono-consommateur (tampons en anneau).

36
Nova

Le point de départ serait soit l’un des articles de la DDJ de Herb Sutter soit pour un producteur unique et consommateur ou pour plusieurs . Le code qu'il donne (en ligne à partir de la deuxième page de chaque article) utilise le type de modèle atomique <T> de style C++ 0x; que vous pouvez imiter à l’aide de la bibliothèque interprocess Boost.

Le code de renforcement est enfoui dans les profondeurs de la bibliothèque interprocess, mais après avoir lu dans le fichier d’en-tête approprié (atomic.hpp) les implémentations nécessaires aux opérations de comparaison et d’échange nécessaires sur les systèmes avec lesquels je suis familier se présente.

25
Steve Gilham

Folly de Facebook semble avoir des structures de données sans verrouillage basées sur C++ 11 <atomic>:

J'oserais dire que ceux-ci sont actuellement utilisés en production, alors je suppose qu'ils pourraient être utilisés en toute sécurité dans d'autres projets.

À votre santé!

15
André Neves

Oui!

J'ai écrit une file d'attente sans verrouillage . Il a des fonctionnalités ™:

  • Entièrement sans attente (pas de boucles CAS)
  • Super rapide (plus de cent millions d'opérations de mise en file d'attente/de retrait de la file d'attente par seconde)
  • Utilise la sémantique de déplacement C++ 11
  • Se développe au besoin (mais seulement si vous le souhaitez)
  • Est-ce que la gestion de la mémoire sans verrou pour les éléments (en utilisant des blocs contigus pré-alloués)
  • Autonome (deux en-têtes plus une licence et un readme)
  • Compile sous MSVC2010 +, Intel ICC 13 et GCC 4.7.2 (et devrait fonctionner avec tout compilateur entièrement compatible C++ 11)

C'est disponible sur GitHub sous la licence BSD simplifiée (n'hésitez pas à le bifurquer!).

Mises en garde:

  • Uniquement pour l'architecture mono-producteur mono-consommateur (c'est-à-dire deux threads)
  • Bien testé sur x86 (-64), il devrait fonctionner sur les processeurs ARM, PowerPC et autres où les entiers alignés de taille native, les charges et les pointeurs de taille native sont naturellement atomiques, mais n'a pas été testé sur le terrain avec des processeurs non x86 (si un pour le tester faites le moi savoir)
  • Aucune idée si des brevets sont enfreints (utilisation à vos risques et périls, etc.). Notez que je l’ai conçu et mis en œuvre moi-même à partir de zéro.
14
Cameron

Il y a une telle bibliothèque, mais c'est en C.

L'emballage en C++ devrait être simple.

http://www.liblfds.org

11
user82238

boost.lockfree tente de créer des implémentations c ++ des classes lockfree stack et fifo.

référentiel git public

10
tim

Après avoir vérifié la plupart des réponses données, je ne peux que déclarer: 

La réponse est NO

Il n’existe aucun droit de ce type qui pourrait être utilisé dès la sortie de la boîte.

10
RED SOFT ADAIR

La chose la plus proche dont je suis au courant est Listes Windows liées entre elles étroitement imbriquées . Bien sûr, il s’agit uniquement de Windows.

6

Si vous avez une file d'attente/FIFO multi-producteurs/mono-consommateur, vous pouvez facilement créer un LockFree en utilisant SLIST ou une pile triviale Lock Free LIFO. Ce que vous faites est d’avoir une deuxième pile "privée" pour le consommateur (ce qui peut également être fait en tant que SLIST pour la simplicité ou tout autre modèle de pile que vous choisissez). Le consommateur supprime des éléments de la pile privée. Chaque fois que le LIFO privé est épuisé, vous effectuez un nettoyage plutôt que de supprimer le SLIST concurrent partagé (en saisissant toute la chaîne SLIST), puis vous déplacez la liste Flushed dans l'ordre en plaçant des éléments dans la pile privée.

Cela fonctionne pour un producteur/consommateur unique et pour plusieurs producteurs/consommateur unique.

Cependant, cela ne fonctionne pas pour les cas impliquant plusieurs consommateurs (avec un seul producteur ou avec plusieurs producteurs).

En outre, pour ce qui est des tables de hachage, elles constituent un candidat idéal pour la "segmentation", qui consiste simplement à diviser le hachage en segments comportant un verrou par segments du cache. Voici comment la bibliothèque simultanée Java le fait (en utilisant 32 bandes). Si vous avez un verrou de lecteur-graveur léger, vous pouvez accéder simultanément à la table de hachage pour des lectures simultanées et vous ne bloquerez que lorsque l'écriture aura lieu sur des bandes contestées (et éventuellement si vous autorisez la croissance de la table de hachage).

Si vous obtenez le vôtre, veillez à bien entrelacer vos verrous avec les entrées de hachage plutôt que de placer tous vos verrous dans un tableau les uns à côté des autres afin d'éviter tout risque de faux partage.

5
Adisak

Et puis Intel Threading Building Blocks est venu. Et pour un temps, c'était bon.

PS: vous recherchez concurrent_queue et concurrent_hash_map

3
Edouard A.

Je peux venir un peu en retard à ce sujet.

L'absence de solutions (à la question posée) est principalement due à un problème important en C++ (avant C++ 0x/11): C++ n'a (a) pas de modèle de mémoire concurrente.

Maintenant, en utilisant std :: atomic, vous pouvez contrôler les problèmes d’ordre de la mémoire et effectuer les opérations de comparaison et d’échange appropriées. J'ai moi-même écrit une implémentation de la file d'attente sans verrou de Micheal & Scott (PODC96) en utilisant C++ 11 et les Hazard Pointers de Micheal (IEEE TPDS 2004) pour éviter les problèmes précoces de libre accès et d'ABA. Cela fonctionne bien, mais c'est une mise en œuvre rapide et sale et je ne suis pas satisfait des performances réelles. Le code est disponible sur bitbucket: LockFreeExperiment

Il est également possible d'implémenter une file d'attente sans verrouillage sans pointeurs de danger en utilisant des mots doubles CAS (mais les versions 64 bits ne seront possibles que sur les x86-64 en utilisant cmpxchg16b), voici un article de blog à ce sujet (avec du code non testé pour la file d'attente) : Implémentation de la comparaison générique double-mot et échange pour x86/x86-64 (blog LSE.)

Mes propres tests me montrent que la file d'attente à double verrouillage (également dans l'article de Micheal & Scott 1996) fonctionne aussi bien que celle sans verrouillage (je n'ai pas suffisamment discuté pour que les structures de données verrouillées présentent des problèmes de performances, mais mon banc est trop léger pour maintenant) et la file d'attente simultanée du TBB d'Intel semble encore meilleure (deux fois plus vite) pour un nombre relativement petit (selon le système d'exploitation, sous FreeBSD 9, la limite la plus basse que j'ai trouvée jusqu'à présent, ce nombre est de 8 threads sur une i7 avec 4 ht-core, et donc 8 processeurs logiques) de threads et ont un comportement très étrange (le temps d'exécution de mon benchmark simple passe de quelques secondes à quelques heures!)

Une autre limitation concernant les files d'attente sans verrouillage suivant le style STL: avoir des itérateurs sur une file d'attente sans verrouillage n'a aucun sens.

2
Marwan Burelle

À ma connaissance, il n’existe pas encore de telles informations. Un problème qu'un installateur doit résoudre est la nécessité d'un allocateur de mémoire sans verrou, qui existe, bien que je ne puisse pas trouver le lien pour le moment.

1
Tobias

Ce qui suit est tiré de l’article de Herb Sutter sur Quurrent http://www.drdobbs.com/parallel/writing-a-generalized-concurrent-queue/211601363?pgno=1 . J'ai apporté des modifications telles que réorganiser le compilateur. Il faut GCC v4.4 + pour compiler ce code.

#include <atomic>
#include <iostream>
using namespace std;

//compile with g++ setting -std=c++0x

#define CACHE_LINE_SIZE 64

template <typename T>
struct LowLockQueue {
private:
    struct Node {
    Node( T* val ) : value(val), next(nullptr) { }
    T* value;
    atomic<Node*> next;
    char pad[CACHE_LINE_SIZE - sizeof(T*)- sizeof(atomic<Node*>)];
    };
    char pad0[CACHE_LINE_SIZE];

// for one consumer at a time
    Node* first;

    char pad1[CACHE_LINE_SIZE
          - sizeof(Node*)];

// shared among consumers
    atomic<bool> consumerLock;

    char pad2[CACHE_LINE_SIZE
          - sizeof(atomic<bool>)];

// for one producer at a time
    Node* last;

    char pad3[CACHE_LINE_SIZE
          - sizeof(Node*)];

// shared among producers
    atomic<bool> producerLock;

    char pad4[CACHE_LINE_SIZE
          - sizeof(atomic<bool>)];

public:
    LowLockQueue() {
    first = last = new Node( nullptr );
    producerLock = consumerLock = false;
    }
    ~LowLockQueue() {
    while( first != nullptr ) {      // release the list
        Node* tmp = first;
        first = tmp->next;
        delete tmp->value;       // no-op if null
        delete tmp;
    }
    }

    void Produce( const T& t ) {
    Node* tmp = new Node( new T(t) );
    asm volatile("" ::: "memory");                            // prevent compiler reordering
    while( producerLock.exchange(true) )
        { }   // acquire exclusivity
    last->next = tmp;         // publish to consumers
    last = tmp;             // swing last forward
    producerLock = false;       // release exclusivity
    }

    bool Consume( T& result ) {
    while( consumerLock.exchange(true) )
        { }    // acquire exclusivity
    Node* theFirst = first;
    Node* theNext = first-> next;
    if( theNext != nullptr ) {   // if queue is nonempty
        T* val = theNext->value;    // take it out
        asm volatile("" ::: "memory");                            // prevent compiler reordering
        theNext->value = nullptr;  // of the Node
        first = theNext;          // swing first forward
        consumerLock = false;             // release exclusivity
        result = *val;    // now copy it back
        delete val;       // clean up the value
        delete theFirst;      // and the old dummy
        return true;      // and report success
    }
    consumerLock = false;   // release exclusivity
    return false;                  // report queue was empty
    }
};

int main(int argc, char* argv[])
{
    //Instead of this Mambo Jambo one can use pthreads in Linux to test comprehensively
LowLockQueue<int> Q;
Q.Produce(2);
Q.Produce(6);

int a;
Q.Consume(a);
cout<< a << endl;
Q.Consume(a);
cout<< a << endl;

return 0;
}
1
enthusiasticgeek

Je l'ai écrit à un moment donné probablement en 2010, j'en suis certain, avec l'aide de différentes références. Ce consommateur unique multi-producteurs.

template <typename T>
class MPSCLockFreeQueue 
{
private:
    struct Node 
    {
        Node( T val ) : value(val), next(NULL) { }
        T value;
        Node* next;
    };
    Node * Head;               
    __declspec(align(4)) Node * InsertionPoint;  //__declspec(align(4)) forces 32bit alignment this must be changed for 64bit when appropriate.

public:
    MPSCLockFreeQueue() 
    {
        InsertionPoint = new Node( T() );
        Head = InsertionPoint;
    }
    ~MPSCLockFreeQueue() 
    {
        // release the list
        T result;
        while( Consume(result) ) 
        {   
            //The list should be cleaned up before the destructor is called as there is no way to know whether or not to delete the value.
            //So we just do our best.
        }
    }

    void Produce( const T& t ) 
    {
        Node * node = new Node(t);
        Node * oldInsertionPoint = (Node *) InterLockedxChange((volatile void **)&InsertionPoint,node);
        oldInsertionPoint->next = node;
    }

    bool Consume( T& result ) 
    {
        if (Head->next)
        {
            Node * oldHead = Head;
            Head = Head->next;
            delete oldHead;
            result = Head->value;
            return true;
        }       
        return false;               // else report empty
    }

};
0
curlyhairedgenius

J'ai trouvé une autre solution écrite en c:

http://www.ddj.com/hpc-high-performance-computing/219500200

0
RED SOFT ADAIR