Existe-t-il des conteneurs simultanés dans C ++ 11?

En particulier, je recherche une queue bloquante. Y a-t-il une telle chose en C ++ 11? Si non, quelles sont mes autres options? Je ne veux vraiment plus descendre au niveau du thread. Beaucoup trop enclin aux erreurs.

Selon Diego Dagum de Microsoft Visual C ++ Team :

Une question récurrente (eh bien, l’une des nombreuses) concerne les conteneurs STL et s’ils sont thread-safe.

En prenant les mots de Stephan ici, la réalité est qu’ils ne le sont pas, non pas comme un bug mais comme une caractéristique: avoir chaque fonction membre de chaque conteneur STL qui acquiert un verrou interne annihilerait les performances. En tant que bibliothèque polyvalente et hautement réutilisable, elle ne serait pas non plus correcte: le niveau correct pour placer les verrous dépend de ce que fait le programme. En ce sens, les fonctions individuelles des membres ne sont pas si correctes.

La bibliothèque de modèles parallèles (PPL) comprend plusieurs conteneurs qui fournissent un access sécurisé à leurs éléments:

  • La classe concurrent_vector est une classe de conteneur de séquence qui permet un access aléatoire à tout élément. Il active les opérations append, access element, access iterator et iterator traversal.
  • La classe concurrent_queue est une classe de conteneur de séquence qui permet un access premier entré, premier sorti à ses éléments. Il permet un nombre limité d’opérations à access simultané, telles que push et try_pop, pour n’en citer que quelques-unes.

Quelques échantillons ici .

Aussi intéressant: http://www.justsoftwaresolutions.co.uk/threading/implementing-a-thread-safe-queue-using-condition-variables.html .

C ++ 11 ne fournit pas de conteneurs simultanés. Cependant, il existe des options de bibliothèque. Outre la PPL déjà mentionnée, n’oubliez pas la bibliothèque Intel TBB.

Il dispose d’une hash_map simultanée de la queue , hash_map , set et vector . Mais ce n’est pas seulement une bibliothèque de conteneurs sécurisée pour les threads, elle est également livrée avec une version parallèle des algorithmes standard (for-loop, reduction, sort, …).

Site Web Intel TBB

Je suis surpris que personne n’ait mentionné moodycamel :: ConcurrentQueue . Nous l’utilisons depuis un certain temps et il fonctionne très bien. Il est spécifique que son implémentation soit sans blocage, ce qui apporte immédiatement une vitesse énorme. Autres raisons de l’utiliser (en citant le site officiel):

Il n’y a pas beaucoup de files d’attente sans verrou à part entière pour C ++. Boost en a un, mais il est limité aux objects avec des opérateurs d’affectation sortingviaux et des destructeurs sortingviaux, par exemple. La queue TBB d’Intel n’est pas libre de tout locking et nécessite également des constructeurs sortingviaux. Il existe de nombreux articles académiques qui implémentent des files d’attente sans locking en C ++, mais le code source utilisable est difficile à trouver et le teste encore plus.

Quelques repères et comparaisons sont disponibles ici , ici et ici .

Les interfaces des conteneurs n’ont tout simplement pas été conçues avec cet objective. Pour les interfaces qu’ils utilisent, un verrou visible pour le client est vraiment la seule façon d’accomplir cela tout en garantissant l’exactitude et le comportement prévisible. Ce serait aussi terriblement inefficace car le nombre d’acquisitions serait très élevé (par rapport à une bonne mise en œuvre).

Solution 1

Passez par valeur (le cas échéant).

Solution 2

Créez une collection d’implémentations simples que vous pouvez utiliser pour passer des conteneurs tout en maintenant un verrou de scope (considérez-le pseudo c ++):

 template  class t_locked_collection { public: t_locked_collection(TCollection& inCollection, t_lock& lock) : collection(inCollection), d_lock(lock), d_nocopy() { } TCollection& collection; // your convenience stuff private: t_scope_lock d_lock; t_nocopy d_nocopy; }; 

puis l’appelant associe le verrou à la collection, puis vous mettez à jour vos interfaces pour utiliser (passer) le type de conteneur, le cas échéant. C’est juste une extension de classe d’un pauvre homme.

Ce conteneur verrouillé est un exemple simple, et il existe quelques autres variantes. C’est la voie que j’ai choisie car elle vous permet d’utiliser le niveau de granularité idéal pour votre programme, même s’il n’est pas aussi transparent (syntaxiquement) que les méthodes verrouillées. Il est également relativement facile d’adapter les programmes existants. Au moins, il se comporte de manière prévisible, contrairement aux collections avec des verrous internes.

Une autre variante serait:

 template  class t_lockable_collection { public: // ... private: TCollection d_collection; t_mutex d_mutex; }; // example: typedef t_lockable_collection > t_lockable_int_vector; 

… où un type similaire à t_locked_collection pourrait être utilisé pour exposer la collection sous-jacente. Ne pas laisser entendre que cette approche est infaillible, tout simplement infaillible.

Ma version d’une simultanéité concurrente d’espace de noms de carte non ordonnée {

 template class unordered_bucket: private std::unordered_map { mutable std::recursive_mutex m_mutex; public: T1 &operator [](T a) { std::lock_guard l(m_mutex); return std::unordered_map::operator [](a); } size_t size() const noexcept { std::lock_guard l(m_mutex); return std::unordered_map::size(); } vector> toVector() const { std::lock_guard l(m_mutex); vector> ret; for(const pair &p:*this) { ret.push_back(p); } return ret; } bool find(const T &t) const { std::lock_guard l(m_mutex); if(this->std::unordered_map::find(t) == this->end()) return false; //not found return true; } void erase() { std::lock_guard l(m_mutex); this->unordered_map::erase(this->begin(),this->end()); } void erase(const T &t) { std::lock_guard l(m_mutex); this->unordered_map::erase(t); } }; #define BUCKETCOUNT 10 template class ConcurrentMap { std::vector> m_v; public: ConcurrentMap():m_v(BUCKETCOUNT){} //using 10 buckets T1 &operator [](T a) { std::hash h; return m_v[h(a)%BUCKETCOUNT][a]; } size_t size() const noexcept { size_t cnt=0; for(const unordered_bucket &ub:m_v) cnt=cnt+ub.size(); return cnt; } vector> toVector() const { vector> ret; for(const unordered_bucket &u:m_v) { const vector> &data=u.toVector(); ret.insert(ret.end(),data.begin(),data.end()); } return ret; } bool find(const T &t) const { for(const unordered_bucket &u:m_v) if(true == u.find(t)) return true; return false; } void erase() { for(unordered_bucket &u:m_v) u.erase(); } void erase(const T &t) { std::hash h; unordered_bucket &ub = m_v[h(t)%BUCKETCOUNT]; ub.erase(t); } }; }