3 votes

Recherché : Solution élégante à une condition de course

J'ai le code suivant :

class TimeOutException
{};

template <typename T>
class MultiThreadedBuffer
{
public:
    MultiThreadedBuffer()
    {
        InitializeCriticalSection(&m_csBuffer);
        m_evtDataAvail = CreateEvent(NULL, TRUE, FALSE, NULL);
    }
    ~MultiThreadedBuffer()
    {
        CloseHandle(m_evtDataAvail);
        DeleteCriticalSection(&m_csBuffer);
    }
    void LockBuffer()
    {
        EnterCriticalSection(&m_csBuffer);
    }
    void UnlockBuffer()
    {
        LeaveCriticalSection(&m_csBuffer);
    }
    void Add(T val)
    {
        LockBuffer();
        m_buffer.push_back(val);
        SetEvent(m_evtDataAvail);
        UnlockBuffer();
    }
    T Get(DWORD timeout)
    {
        T val;
        if (WaitForSingleObject(m_evtDataAvail, timeout) == WAIT_OBJECT_0) {
            LockBuffer();

            if (!m_buffer.empty()) {
                val = m_buffer.front();
                m_buffer.pop_front();
            }

            if (m_buffer.empty()) {
                ResetEvent(m_evtDataAvail);
            }

            UnlockBuffer();
        } else {
            throw TimeOutException();
        }
        return val;
    }
    bool IsDataAvail()
    {
        return (WaitForSingleObject(m_evtDataAvail, 0) == WAIT_OBJECT_0);
    }
    std::list<T> m_buffer;
    CRITICAL_SECTION m_csBuffer;
    HANDLE m_evtDataAvail;
};

Les tests unitaires montrent que ce code fonctionne bien lorsqu'il est utilisé sur un seul thread, tant que le constructeur par défaut de T et les opérateurs de copie/affectation ne lancent pas. Puisque j'écris T, c'est acceptable.

Mon problème est la méthode Get. Si aucune donnée n'est disponible (c'est-à-dire que m_evtDataAvail n'est pas défini), quelques threads peuvent bloquer sur l'appel WaitForSingleObject. Lorsque de nouvelles données sont disponibles, ils passent tous par l'appel Lock(). Un seul d'entre eux passera et pourra récupérer les données et passer à autre chose. Après l'appel Unlock(), un autre thread peut passer et constater qu'il n'y a pas de données. Actuellement, il renvoie l'objet par défaut.

Ce que je veux, c'est que ce deuxième thread (et les autres) reviennent à l'appel WaitForSingleObject. Je pourrais ajouter un bloc else qui déverrouille et fait un goto, mais c'est tout simplement diabolique.

Cette solution ajoute également la possibilité d'une boucle sans fin puisque chaque retour redémarre le délai d'attente. Je pourrais ajouter du code pour vérifier l'horloge à l'entrée et ajuster le délai à chaque retour, mais alors cette simple méthode Get devient très compliquée.

Avez-vous des idées sur la façon de résoudre ces problèmes tout en maintenant la testabilité et la simplicité ?

Oh, pour ceux qui se demandent, la fonction IsDataAvail n'existe que pour les tests. Elle ne sera pas utilisée dans le code de production. Les fonctions Add et Get sont les seules méthodes qui seront utilisées dans un environnement non-test.

7voto

Naveen Points 37095

Vous devez créer un événement de réinitialisation automatique au lieu d'un événement de réinitialisation manuelle. Cela garantit que si plusieurs threads sont en attente d'un événement, et que lorsque l'événement est activé, un seul thread sera libéré. Tous les autres threads resteront en état d'attente. Vous pouvez créer un événement auto-reset en passant FALSE au second paramètre de la fonction CreateEvent API. Notez également que ce code n'est pas protégé contre les exceptions, c'est-à-dire qu'après avoir verrouillé le tampon, si une instruction lève une exception, votre section critique ne sera pas déverrouillée. Utilisez RAII pour s'assurer que votre section critique est débloquée même en cas d'exception.

5voto

Chris Dodd Points 1990

Vous pourriez utiliser un objet Sémaphore au lieu d'un objet Événement générique. Le nombre de sémaphores devrait être initialisé à 0 et incrémenté de 1 avec ReleaseSemaphore à chaque fois que Add est appelé. De cette façon, le WaitForSingleObject de Get ne libérera jamais plus de threads pour lire le tampon qu'il n'y a de valeurs dans le tampon.

3voto

Remus Rusanu Points 159382

Vous devrez toujours coder pour le cas où l'événement est signalé mais qu'il n'y a pas de données, même AVEC des événements à réinitialisation automatique. Il existe une condition de course à partir du moment où WaitForsingleevent se réveille jusqu'à ce que LockBuffer soit appelé, et dans cet intervalle, un autre thread peut extraire les données du tampon. Votre code doit placer WaitForSingleEvent dans une boucle. Diminuez le timeout avec le temps déjà passé dans chaque itération de la boucle...

En guise d'alternative, puis-je vous intéresser à des solutions plus évolutives et performantes ? Listes singulièrement liées imbriquées pool de threads du système d'exploitation QueueUserWorkItem y idempotent traitement. Ajouter pousse une entrée dans la liste et soumet un work item. Le bon de travail pops une entrée et si elle n'est pas NULL, la traiter. Il est possible de faire preuve de fantaisie et d'avoir une logique supplémentaire pour que le processeur boucle et garde un état marquant sa présence "active" afin que l'Add ne demande pas d'éléments de travail inutiles, mais ce n'est pas strictement nécessaire. Pour une vitesse encore plus élevée et une répartition de la charge entre plusieurs cœurs et plusieurs processeurs, je recommande d'utiliser des ports d'achèvement en file d'attente. Les détails sont décrits dans les articles de Rick Vicik, j'ai un article de blog qui relie les 3 en même temps : Programmes Windows haute performance .

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X