Windows events & pthread: WaitForSingleObject

Your first design may seem like a solution, but it's usually just an early definition of the problem you are trying to solve.

— Luke Wroblewski

L'évènement — Event — constitue sans doute la primitive de synchronisation la plus employée par les applications développées pour la plateforme Windows.

Malgré son usage important, l'évènement demeure un objet — kernel object — de synchronisation relativement primitif. En substance, un évènement ne contient que deux booléens. Un premier détermine son type, manuel ou automatique, alors que le second détermine son état, signalé ou non.

Lorsqu'une attente est effectuée contre un évènement, la sémantique de l'opération varie selon selon son type. Ainsi, lorsqu'un évènement manuel est signalé, ce dernier réveillera tous les fils d'exécution — threads — bloqués. Au contraire, lorsqu'un évènement automatique est signalé, un seul fil d'exécution en attente sera réveillé. Par ailleurs, un évènement automatique consomme son signal dès qu'un fil d'exécution est réveillé. Autrement dit, un évènement automatique retourne à l'état non signalé dès qu'un fil d'exécution complète une attente.

Quiconque porte du code natif Windows vers une plateforme tierce se voit placé dans l'obligation d'émuler, plus ou moins fidèlement, les évènements. La complexité de cette couche d'émulation dépend du type d'attente supporté, soit l'attente simple (WaitForSingleObject) ou multiple (WaitForMultipleObjects). Dans cet article, une implantation des évènements basée sur pthread et supportant uniquement l'attente simple sera développée. L'article suivant, Windows events & pthread: WaitForMultipleObjects, présente une évolution de cette implantation supportant l'attente multiple.

EventBase

Il est à toutes fins pratiques possible de reproduire entièrement le comportement des évènements, autant automatiques que manuels, à l'aide d'une variable de condition, un verrou — mutex, lock — et un booléen.

Cette infrastructure commune est placée dans une classe de base qui se charge entre autre d'initialiser et détruire le verrou et la variable de condition.

class EventBase {
protected:
    EventBase(bool signaled) : signaled(signaled) {
        pthread_cond_init(&cond, NULL);
        pthread_mutex_init(&lock, NULL);
    }

    ~EventBase() {
        pthread_cond_destroy(&cond);
        pthread_mutex_destroy(&lock);
    }

public:
    bool reset() {
        pthread_mutex_lock(&lock);
        bool old = signaled;
        signaled = false;
        pthread_mutex_unlock(&lock);
        return old;
    }

protected:
    pthread_mutex_t lock;
    pthread_cond_t cond;
    bool signaled;
};

ManualEvent

Les évènements manuels sont certainement les plus simples à implanter. Lorsqu'un évènement manuel est signalé, il s'agit essentiellement d'effectuer un diffusion — broadcast — sur la variable de condition afin de réveiller tous les fil d'exécution en attente. L'attente se résume, quant à elle, à bloquer, au besoin, sur une variable de condition.

class ManualEvent : public EventBase {
public:
    ManualEvent(bool signaled = false) :
        EventBase(signaled) {}

    void wait() {
        pthread_mutex_lock(&lock);
        if (!signaled)
            pthread_cond_wait(&cond, &lock);
        pthread_mutex_unlock(&lock);
    }

    void signal() {
        pthread_mutex_lock(&lock);
        signaled = true;
        pthread_mutex_unlock(&lock);
        pthread_cond_broadcast(&cond);
    }
};

AutoEvent

La logique des évènements automatiques est un peu plus complexe. Cette complexité additionnelle vient du fait qu'il importe de conserver un compte du nombre de fils d'exécution en attente. Cette gestion est nécessaire car un évènement automatique ne réveille qu'un fil d'exécution à la fois lorsqu'il est signalé. Ainsi, avant de faire passer l'objet à l'état signalé, il importe de déterminer s'il reste des fils d'exécution bloqués sur l'évènement.

class AutoEvent : public EventBase {
public:
    AutoEvent(bool signaled = false) :
        EventBase(signaled), waiters(0) {}

    void wait() {
        pthread_mutex_lock(&lock);
        if (signaled) {
            signaled = false;
        } else {
            waiters++;
            pthread_cond_wait(&cond, &lock);
            waiters--;
        }
        pthread_mutex_unlock(&lock);
    }

    void signal() {
        pthread_mutex_lock(&lock);
        if (waiters == 0) {
            signaled = true;
        } else {
            pthread_cond_signal(&cond);
        }
        pthread_mutex_unlock(&lock);
    }

private:
    unsigned int waiters;
};

Polymorphisme

L'implantation des classes EventBase, ManualEvent et AutoEvent présente le défaut de ne pas être polymorphique. L'absence de méthode virtuelle assure des performances maximales, mais peut rapidement devenir handicapante. Heureusement, en joignant hérédité multiple et templates, il est possible d'ajouter du polymorphisme de manière ad hoc.

class Event {
public:
    virtual ~Event() {}
    virtual void wait() = 0;
    virtual void signal() = 0;
    virtual bool reset() = 0;
};

template <typename Impl>
class EventType : public Event, public Impl {
public:
    EventType(bool signaled = false) : Impl(signaled) {}

    virtual void wait() { Impl::wait(); }
    virtual void signal() { Impl::signal(); }
    virtual bool reset() { return Impl::reset(); }

private:
    EventType(const EventType<Impl>&);
    void operator=(EventType<Impl>);
};

Ainsi, lorsqu'un évènement doit être utilisé d'une manière polymorphique, un client peut tout simplement allouer un EventType paramétré sur le type d'évènement désiré.

Event* ev1 = new EventType<ManualEvent>(false);
Event* ev2 = new EventType<AutoEvent>(false);

Conclusion

Supporter l'attente simple, c'est-à-dire la sémantique de la fonction WaitForSingleObject est une tâche relativement aisée, la correspondance avec la sémantique des fonctions de pthread étant presque directe.

Certains éléments pourraient être améliorés dans l'implantation des évènements présentée dans cet article. En particulier, l'utilisation de lock guards simplifierait certainement la logique.

Le code source peut être consulté en ligne: event.cpp.

L'article Windows events & pthread: WaitForMultipleObjects présente une évolution de l'implantation présentée ci-dessus supportant l'attente multiple.

Lexique