web-dev-qa-db-fra.com

Arrêt de C ++ 11 std :: threads en attente sur std :: condition_variable

J'essaie de comprendre les mécanismes de base du multithreading dans le nouveau standard C++ 11. L'exemple le plus élémentaire auquel je peux penser est le suivant:

  • Un producteur et un consommateur sont implémentés dans des threads séparés
  • Le producteur place une certaine quantité d'articles dans une file d'attente
  • Le consommateur prend des articles de la file d'attente s'il y en a

Cet exemple est également utilisé dans de nombreux manuels scolaires sur le multithreading et tout sur le processus de communication fonctionne bien. Cependant, j'ai un problème quand il s'agit d'arrêter le fil consommateur.

Je veux que le consommateur s'exécute jusqu'à ce qu'il reçoive un signal d'arrêt explicite (dans la plupart des cas, cela signifie que j'attends la fin du producteur pour pouvoir arrêter le consommateur avant la fin du programme). Malheureusement, les threads C++ 11 n'ont pas de mécanisme d'interruption (que je connais par multithreading dans Java par exemple). Ainsi, je dois utiliser des drapeaux comme isRunning pour signaler que je veux un thread arrêter.

Le problème principal est maintenant: après avoir arrêté le thread producteur, la file d'attente est vide et le consommateur attend sur un condition_variable Pour obtenir un signal lorsque la file d'attente est à nouveau remplie. J'ai donc besoin de réveiller le thread en appelant notify_all() sur la variable avant de quitter.

J'ai trouvé une solution de travail, mais cela semble en quelque sorte en désordre. L'exemple de code est répertorié ci-dessous (je suis désolé mais d'une manière ou d'une autre je n'ai pas pu réduire davantage la taille du code pour un exemple minimal "minimal"):

La classe Queue:

class Queue{
public:
    Queue() : m_isProgramStopped{ false } { }

    void Push(int i){
        std::unique_lock<std::mutex> lock(m_mtx);
        m_q.Push(i);
        m_cond.notify_one();
    }

    int pop(){
        std::unique_lock<std::mutex> lock(m_mtx);
        m_cond.wait(lock, [&](){ return !m_q.empty() || m_isProgramStopped; });

        if (m_isProgramStopped){
            throw std::exception("Program stopped!");
        }

        int x = m_q.front();
        m_q.pop();

        std::cout << "Thread " << std::this_thread::get_id() << " popped " << x << "." << std::endl;
        return x;
    }

    void stop(){
        m_isProgramStopped = true;
        m_cond.notify_all();
    }

private:
    std::queue<int> m_q;
    std::mutex m_mtx;
    std::condition_variable m_cond;
    bool m_isProgramStopped;
};

Le producteur:

class Producer{
public:
    Producer(Queue & q) : m_q{ q }, m_counter{ 1 } { }

    void produce(){
        for (int i = 0; i < 5; i++){
            m_q.Push(m_counter++);
            std::this_thread::sleep_for(std::chrono::milliseconds{ 500 });
        }
    }

    void execute(){
        m_t = std::thread(&Producer::produce, this);
    }

    void join(){
        m_t.join();
    }

private:
    Queue & m_q;
    std::thread m_t;

    unsigned int m_counter;
};

Le consommateur:

class Consumer{
public:
    Consumer(Queue & q) : m_q{ q }, m_takeCounter{ 0 }, m_isRunning{ true }
    { }

    ~Consumer(){
        std::cout << "KILL CONSUMER! - TOOK: " << m_takeCounter << "." << std::endl;
    }

    void consume(){
        while (m_isRunning){
            try{
                m_q.pop();
                m_takeCounter++;
            }
            catch (std::exception e){
                std::cout << "Program was stopped while waiting." << std::endl;
            }
        }
    }

    void execute(){
        m_t = std::thread(&Consumer::consume, this);
    }

    void join(){
        m_t.join();
    }

    void stop(){
        m_isRunning = false;
    }

private:
    Queue & m_q;
    std::thread m_t;

    unsigned int m_takeCounter;
    bool m_isRunning;
};

Et enfin la main():

int main(void){
    Queue q;

    Consumer cons{ q };
    Producer prod{ q };

    cons.execute();
    prod.execute();

    prod.join();

    cons.stop();
    q.stop();

    cons.join();

    std::cout << "END" << std::endl;

    return EXIT_SUCCESS;
}

Est-ce la manière à droite de terminer un thread qui attend une variable de condition ou existe-t-il de meilleures méthodes? Actuellement, la file d'attente doit savoir si le programme s'est arrêté (ce qui à mon avis détruit le couplage lâche des composants) et j'ai besoin d'appeler stop() sur la file d'attente explicitement, ce qui ne semble pas correct.

De plus, la variable de condition qui doit simplement être utilisée comme un singal si la file d'attente est vide représente maintenant une autre condition - si le programme est terminé. Si je ne me trompe pas, chaque fois qu'un thread attend une variable de condition pour qu'un événement se produise, il devrait également vérifier si le thread doit être arrêté avant de continuer son exécution (ce qui semble également incorrect).

Ai-je ces problèmes parce que toute ma conception est défectueuse ou manque-t-il certains mécanismes qui peuvent être utilisés pour quitter les threads de manière propre?

32
Devon Cornwall

Non, il n'y a rien de mal avec votre conception, et c'est l'approche normale adoptée pour ce genre de problème.

Il est parfaitement valable que vous ayez plusieurs conditions (par exemple, quelque chose dans la file d'attente ou l'arrêt de programme) attachées à une variable de condition. L'essentiel est que les bits de la condition soient vérifiés lorsque wait revient.

Au lieu d'avoir un indicateur dans Queue pour indiquer que le programme s'arrête, vous devez considérer l'indicateur comme "puis-je accepter". Il s'agit d'un meilleur paradigme global et fonctionne mieux dans un environnement multi-thread.

De plus, au lieu que pop lève une exception si quelqu'un l'appelle et que stop a été appelée, vous pouvez remplacer la méthode par bool try_pop(int &value) qui renverra true si une valeur a été retournée, sinon false. De cette façon, l'appelant peut vérifier l'échec pour voir si la file d'attente a été arrêtée (ajoutez une méthode bool is_stopped() const). Bien que la gestion des exceptions fonctionne ici, elle est un peu lourde et n'est pas vraiment un cas exceptionnel dans un programme multi-thread.

6
Sean

wait peut être appelé avec un timeout. Le contrôle est renvoyé au thread et stop a pu être vérifié. En fonction de cette valeur, il peut wait sur plusieurs éléments à consommer ou terminer l'exécution. Une bonne introduction au multithreading avec c ++ est Concurrence C++ 11 .

1
knivil