web-dev-qa-db-fra.com

Quel est le meilleur moyen d'attendre plusieurs variables de condition en C++ 11?

D'abord un peu contexte : Je suis en train d'apprendre à utiliser les threads en C++ 11 et, dans ce but, j'essaie de construire une petite classe actor, essentiellement (j'ai laissé le matériel de gestion et de propagation d'exceptions comme):

class actor {
    private: std::atomic<bool> stop;
    private: std::condition_variable interrupt;
    private: std::thread actor_thread;
    private: message_queue incoming_msgs;

    public: actor() 
    : stop(false), 
      actor_thread([&]{ run_actor(); })
    {}

    public: virtual ~actor() {
        // if the actor is destroyed, we must ensure the thread dies too
        stop = true;
        // to this end, we have to interrupt the actor thread which is most probably
        // waiting on the incoming_msgs queue:
        interrupt.notify_all();
        actor_thread.join();
    }

    private: virtual void run_actor() {
        try {
            while(!stop)
                // wait for new message and process it
                // but interrupt the waiting process if interrupt is signaled:
                process(incoming_msgs.wait_and_pop(interrupt));
        } 
        catch(interrupted_exception) {
            // ...
        }
    };

    private: virtual void process(const message&) = 0;
    // ...
};

Chaque acteur s'exécute dans son propre actor_thread, attend un nouveau message entrant le incoming_msgs et le traite lorsqu'un message arrive. 

Le actor_thread est créé avec la actor et doit mourir en même temps. C'est pourquoi j'ai besoin d'un mécanisme d'interruption dans la message_queue::wait_and_pop(std::condition_variable interrupt)

J'exige essentiellement que wait_and_pop bloque jusqu'à ce que A) une nouvelle message arrive ou B) jusqu'à ce que la interrupt soit déclenchée, auquel cas, idéalement, un interrupted_exception doit être lancé.

L’arrivée d’un nouveau message dans le message_queue est actuellement aussi modélisée par un std::condition_variable new_msg_notification:

// ...
// in class message_queue:
message wait_and_pop(std::condition_variable& interrupt) {
    std::unique_lock<std::mutex> lock(mutex);

    // How to interrupt the following, when interrupt fires??
    new_msg_notification.wait(lock,[&]{
        return !queue.empty();
    });
    auto msg(std::move(queue.front()));
    queue.pop();
    return msg;
}

Pour résumer la longue histoire, la question est la suivante: comment puis-je interrompre l’attente d’un nouveau message dans new_msg_notification.wait(...) lorsque la interrupt est déclenchée (sans introduire de délai d’expiration)?

Sinon, la question peut être lue comme suit: Comment puis-je attendre que l’un des deux std::condition_variables soit signalé? 

Une approche naïve semble être de ne pas utiliser std::condition_variable du tout pour l’interruption mais d’utiliser plutôt un indicateur atomique std::atomic<bool> interrupted puis occupé, attendez new_msg_notification avec un très petit délai d’attente jusqu’à ce qu’un nouveau message soit arrivé ou jusqu’au true==interrupted. Cependant, j'aimerais beaucoup éviter d'attendre les travaux.


MODIFIER:

D'après les commentaires et la réponse de Pilcrow, il semble qu'il y a fondamentalement deux approches possibles.

  1. Mettez en file d'attente un message spécial "Terminate", tel que proposé par Alan, Mukunda et Pilcrow. J'ai décidé de ne pas utiliser cette option car je ne sais pas du tout combien de fois la taille de la file d'attente devrait se terminer. Il se peut très bien (comme c’est généralement le cas lorsque je souhaite que quelque chose se termine rapidement) qu'il reste des milliers de messages à traiter dans la file d'attente et qu'il semble inacceptable d'attendre qu'ils soient traités jusqu'à ce que le message de fin reçoive son message. tour.
  2. Implémentez une version personnalisée d'une variable de condition, qui peut être interrompue par un autre thread en transmettant la notification à la variable de condition que le premier thread attend. J'ai opté pour cette approche.

Pour ceux qui sont intéressés, ma mise en œuvre va comme suit. La variable de condition dans mon cas est en fait une semaphore (parce que je les aime plus et parce que j'aime l'exercice). J'ai équipé ce sémaphore avec une variable interrupt associée pouvant être obtenue à partir du sémaphore via semaphore::get_interrupt(). Si maintenant un thread est bloqué dans semaphore::wait(), un autre thread a la possibilité d'appeler semaphore::interrupt::trigger() lors de l'interruption du sémaphore, provoquant le déblocage du premier thread et la propagation d'un interrupt_exception.

struct
interrupt_exception {};

class
semaphore {
    public: class interrupt;
    private: mutable std::mutex mutex;

    // must be declared after our mutex due to construction order!
    private: interrupt* informed_by;
    private: std::atomic<long> counter;
    private: std::condition_variable cond;

    public: 
    semaphore();

    public: 
    ~semaphore() throw();

    public: void 
    wait();

    public: interrupt&
    get_interrupt() const { return *informed_by; }

    public: void
    post() {
        std::lock_guard<std::mutex> lock(mutex);
        counter++;
        cond.notify_one(); // never throws
    }

    public: unsigned long
    load () const {
        return counter.load();
    }
};

class
semaphore::interrupt {
    private: semaphore *forward_posts_to;
    private: std::atomic<bool> triggered;

    public:
    interrupt(semaphore *forward_posts_to) : triggered(false), forward_posts_to(forward_posts_to) {
        assert(forward_posts_to);
        std::lock_guard<std::mutex> lock(forward_posts_to->mutex);
        forward_posts_to->informed_by = this;
    }

    public: void
    trigger() {
        assert(forward_posts_to);
        std::lock_guard<std::mutex>(forward_posts_to->mutex);

        triggered = true;
        forward_posts_to->cond.notify_one(); // never throws
    }

    public: bool
    is_triggered () const throw() {
        return triggered.load();
    }

    public: void
    reset () throw() {
        return triggered.store(false);
    }
};

semaphore::semaphore()  : counter(0L), informed_by(new interrupt(this)) {}

// must be declared here because otherwise semaphore::interrupt is an incomplete type
semaphore::~semaphore() throw()  {
    delete informed_by;
}

void
semaphore::wait() {
    std::unique_lock<std::mutex> lock(mutex);
    if(0L==counter) {
        cond.wait(lock,[&]{
            if(informed_by->is_triggered())
                throw interrupt_exception();
            return counter>0;
        });
    }
    counter--;
}

En utilisant cette semaphore, mon implémentation de file d'attente de messages ressemble maintenant à ceci (en utilisant le sémaphore au lieu du std::condition_variable, je pourrais supprimer le std::mutex:

class
message_queue {    
    private: std::queue<message> queue;
    private: semaphore new_msg_notification;

    public: void
    Push(message&& msg) {
        queue.Push(std::move(msg));
        new_msg_notification.post();
    }

    public: const message
    wait_and_pop() {
        new_msg_notification.wait();
        auto msg(std::move(queue.front()));
        queue.pop();
        return msg;
    }

    public: semaphore::interrupt&
    get_interrupt() const { return new_msg_notification.get_interrupt(); }
};

Ma actor est maintenant capable d'interrompre son thread avec une latence très faible dans son thread. La mise en œuvre actuellement ressemble à ceci:

class
actor {
    private: message_queue
    incoming_msgs;

    /// must be declared after incoming_msgs due to construction order!
    private: semaphore::interrupt&
    interrupt;

    private: std::thread
    my_thread;

    private: std::exception_ptr
    exception;

    public:
    actor()
    : interrupt(incoming_msgs.get_interrupt()), my_thread(
        [&]{
            try {
                run_actor();
            }
            catch(...) {
                exception = std::current_exception();
            }
        })
    {}

    private: virtual void
    run_actor() {
        while(!interrupt.is_triggered())
            process(incoming_msgs.wait_and_pop());
    };

    private: virtual void
    process(const message&) = 0;

    public: void
    notify(message&& msg_in) {
        incoming_msgs.Push(std::forward<message>(msg_in));
    }

    public: virtual
    ~actor() throw (interrupt_exception) {
        interrupt.trigger();
        my_thread.join();
        if(exception)
            std::rethrow_exception(exception);
    }
};
25
Julian

Tu demandes,

Quel est le meilleur moyen d'attendre plusieurs variables de condition en C++ 11?

Vous ne pouvez pas et devez refondre. Un thread ne peut attendre que sur une variable de condition (et son mutex associé) à la fois. À cet égard, les installations Windows pour la synchronisation sont plutôt plus riches que celles de la famille de primitives de synchronisation "de style POSIX".

L'approche typique avec les files d'attente thread-safe est de mettre en file d'attente une spéciale "tout est fait!" message, ou pour concevoir une file d'attente "cassable" (ou "shutdown -able"). Dans ce dernier cas, la variable de condition interne de la file protège alors un prédicat complexe: un élément est disponible ou la file a été interrompue.

Dans un commentaire, vous remarquez que

un notify_all () n'aura aucun effet s'il n'y a personne en attente

C'est vrai mais probablement pas pertinent. wait()ing sur une variable de condition implique également la vérification d'un prédicat et son contrôle avant bloquant réellement une notification. Ainsi, un thread de travail occupé à traiter un élément de la file d'attente qui "manque" une notify_all() verra, lors de la prochaine inspection de la condition de la file d'attente, que le prédicat (un nouvel élément est disponible ou que la file d'attente est terminée) a été modifié.

13
pilcrow

Récemment, j'ai résolu ce problème à l'aide d'une variable de condition unique et d'une variable booléenne distincte pour chaque producteur/travailleur. Le prédicat de la fonction wait dans le thread consommateur peut vérifier ces indicateurs et décider quel producteur/travailleur a satisfait à la condition. 

3
Aditya Kumar

Peut-être que cela peut fonctionner:

se débarrasser de l'interruption.

 message wait_and_pop(std::condition_variable& interrupt) {
    std::unique_lock<std::mutex> lock(mutex);
    {
        new_msg_notification.wait(lock,[&]{
            return !queue.empty() || stop;
        });

        if( !stop )
        {
            auto msg(std::move(queue.front()));
            queue.pop();
            return msg;
        }
        else
        {
            return NULL; //or some 'terminate' message
        }
}

Dans destructeur, remplacez interrupt.notify_all() par new_msg_notification.notify_all()

0
Davidb