web-dev-qa-db-fra.com

File d'attente sécurisée pour les threads C++ 11

Un projet sur lequel je travaille utilise plusieurs threads pour travailler sur une collection de fichiers. Chaque thread peut ajouter des fichiers à la liste des fichiers à traiter. J'ai donc créé (ce que je pensais être à l'origine) une file d'attente sécurisée pour les threads. Les portions pertinentes suivent:

// qMutex is a std::mutex intended to guard the queue
// populatedNotifier is a std::condition_variable intended to
//                   notify waiting threads of a new item in the queue

void FileQueue::enqueue(std::string&& filename)
{
    std::lock_guard<std::mutex> lock(qMutex);
    q.Push(std::move(filename));

    // Notify anyone waiting for additional files that more have arrived
    populatedNotifier.notify_one();
}

std::string FileQueue::dequeue(const std::chrono::milliseconds& timeout)
{
    std::unique_lock<std::mutex> lock(qMutex);
    if (q.empty()) {
        if (populatedNotifier.wait_for(lock, timeout) == std::cv_status::no_timeout) {
            std::string ret = q.front();
            q.pop();
            return ret;
        }
        else {
            return std::string();
        }
    }
    else {
        std::string ret = q.front();
        q.pop();
        return ret;
    }
}

Cependant, je commets parfois des erreurs de segmentation à l'intérieur du bloc if (...wait_for(lock, timeout) == std::cv_status::no_timeout) { }, et l'inspection dans gdb indique que les erreurs de segmentation se produisent car la file d'attente est vide. Comment est-ce possible? Je croyais comprendre que wait_for ne renvoie que cv_status::no_timeout après l'avoir notifié, et que cela ne devrait se produire que lorsque FileQueue::enqueue vient de placer un nouvel élément dans la file d'attente.

52
Matt Kline

Selon la norme, les condition_variables sont autorisés à se réveiller de manière fausse, même si l'événement ne s'est pas produit. En cas de réveil parasite, il renverra cv_status::no_timeout (puisqu'il s'est réveillé au lieu de l'expiration), même s'il n'a pas été averti. La bonne solution à cela est bien sûr de vérifier si le réveil était réellement légitime avant de procéder.

Les détails sont spécifiés dans la norme §30.5.1 [thread.condition.condvar]:

—La fonction se débloquera si elle est signalée par un appel à notify_one (), un appel à notify_all (), l'expiration du délai d'expiration absolu (30.2.4) spécifié par abs_time ou de manière parasite. 

... 

Retourne: cv_status :: timeout si le délai absolu (30.2.4) spécifié par abs_time a expiré, other-ise cv_status :: no_timeout.

26
Grizzly

En le regardant, lorsque vous vérifiez une variable de condition, il est préférable d’utiliser une boucle while (afin qu’elle revienne si elle se réveille et n’est pas invalide) Je viens d'écrire un modèle pour une file d'attente asynchrone, j'espère que cela vous aidera.

#ifndef SAFE_QUEUE
#define SAFE_QUEUE

#include <queue>
#include <mutex>
#include <condition_variable>

// A threadsafe-queue.
template <class T>
class SafeQueue
{
public:
  SafeQueue(void)
    : q()
    , m()
    , c()
  {}

  ~SafeQueue(void)
  {}

  // Add an element to the queue.
  void enqueue(T t)
  {
    std::lock_guard<std::mutex> lock(m);
    q.Push(t);
    c.notify_one();
  }

  // Get the "front"-element.
  // If the queue is empty, wait till a element is avaiable.
  T dequeue(void)
  {
    std::unique_lock<std::mutex> lock(m);
    while(q.empty())
    {
      // release lock as long as the wait and reaquire it afterwards.
      c.wait(lock);
    }
    T val = q.front();
    q.pop();
    return val;
  }

private:
  std::queue<T> q;
  mutable std::mutex m;
  std::condition_variable c;
};
#endif
43
ChewOnThis_Trident

Voici probablement comment vous devriez le faire:

void Push(std::string&& filename)
{
    {
        std::lock_guard<std::mutex> lock(qMutex);

        q.Push(std::move(filename));
    }

    populatedNotifier.notify_one();
}

bool try_pop(std::string& filename, std::chrono::milliseconds timeout)
{
    std::unique_lock<std::mutex> lock(qMutex);

    if(!populatedNotifier.wait_for(lock, timeout, [this] { return !q.empty(); }))
        return false;

    filename = std::move(q.front());
    q.pop();

    return true;    
}
12
ronag

Ajoutant à la réponse acceptée, je dirais qu’implémenter une file d’attente correcte multi-producteurs/multi-consommateurs est difficile (plus facile depuis C++ 11, cependant).

Je vous suggère d'essayer la (très bonne) bibliothèque lock free boost , la structure "file" fera tout ce que vous voulez, avec des garanties sans attente/sans verrouillage et sans la nécessité d'un C + +11 compilateur .

J'ajoute cette réponse maintenant parce que la bibliothèque sans verrou est assez nouvelle pour booster (depuis 1.53 je crois)

10
quantdev

Je voudrais réécrire votre fonction de retrait en tant que:

std::string FileQueue::dequeue(const std::chrono::milliseconds& timeout)
{
    std::unique_lock<std::mutex> lock(qMutex);
    while(q.empty()) {
        if (populatedNotifier.wait_for(lock, timeout) == std::cv_status::timeout ) 
           return std::string();
    }
    std::string ret = q.front();
    q.pop();
    return ret;
}

Il est plus court et n’a pas de code en double comme vous l’avez fait. Seul problème, il peut attendre plus longtemps que le délai d'attente. Pour éviter cela, vous devez vous rappeler l'heure de début avant la boucle, vérifiez le délai et réglez le temps d'attente en conséquence. Ou spécifiez le temps absolu sur condition d'attente.

5
Slava

Il existe également une solution GLib pour ce cas, je ne l'ai pas encore essayée, mais je pense que c'est une bonne solution . https://developer.gnome.org/glib/2.36/glib-Asynchronous-Queues. html # g-async-queue-new

1
ransh

BlockingCollection est une classe de collection sécurisée pour les threads C++ 11 qui prend en charge les conteneurs de files d'attente, de piles et de priorités. Il gère le scénario de la file d'attente "vide" que vous avez décrit. Ainsi qu'une file d'attente "complète".

1
gm127

Vous pouvez aimer lfqueue, https://github.com/Taymindis/lfqueue . Il s’agit d’une file d’attente concurrente sans verrou. Je l'utilise actuellement pour consommer la file d'attente de plusieurs appels entrants et fonctionne comme un charme.

0
woon minika