web-dev-qa-db-fra.com

Comment implémenteriez-vous votre propre verrou de lecture / écriture en C ++ 11?

J'ai un ensemble de structures de données que je dois protéger avec un verrou de lecture/écriture. Je connais boost :: shared_lock, mais j'aimerais avoir une implémentation personnalisée en utilisant std :: mutex, std :: condition_variable et/ou std :: atomic afin que je puisse mieux comprendre comment cela fonctionne (et le peaufiner plus tard) .

Chaque structure de données (mobile, mais non copiable) héritera d'une classe appelée Commons qui encapsule le verrouillage. J'aimerais que l'interface publique ressemble à ceci:

class Commons {
public:
    void read_lock();
    bool try_read_lock();
    void read_unlock();

    void write_lock();
    bool try_write_lock();
    void write_unlock();
};

... afin qu'il puisse être hérité publiquement par certains:

class DataStructure : public Commons {};

J'écris du code scientifique et je peux généralement éviter les courses de données; ce verrou est surtout une protection contre les erreurs que je ferai probablement plus tard. Ainsi, ma priorité est une faible surcharge de lecture, donc je n'entrave pas trop un programme correctement exécuté. Chaque thread s'exécutera probablement sur son propre cœur de processeur.

Pourriez-vous s'il vous plaît me montrer (le pseudocode est ok) un verrou de lecture/écriture? Ce que j'ai maintenant est censé être la variante qui empêche la famine des écrivains. Jusqu'à présent, mon principal problème a été l'écart dans read_lock entre vérifier si une lecture est sûre pour réellement incrémenter le nombre de lecteurs, après quoi write_lock sait attendre.

void Commons::write_lock() {
    write_mutex.lock();
    reading_mode.store(false);
    while(readers.load() > 0) {}
}

void Commons::try_read_lock() {
    if(reading_mode.load()) {
        //if another thread calls write_lock here, bad things can happen
        ++readers; 
        return true;
    } else return false;
}

Je suis un peu nouveau dans le multithreading, et j'aimerais vraiment le comprendre. Merci d'avance pour votre aide!

37
jack

Voici un pseudo-code pour un verrou de lecture/écriture ver simple utilisant un mutex et une variable de condition. L'API mutex devrait être explicite. Les variables de condition sont supposées avoir un membre wait(Mutex&) qui (atomiquement!) Supprime le mutex et attend que la condition soit signalée. La condition est signalée par signal() qui réveille un serveur, ou signal_all() qui réveille tous les serveurs .

read_lock() {
  mutex.lock();
  while (writer)
    unlocked.wait(mutex);
  readers++;
  mutex.unlock();
}

read_unlock() {
  mutex.lock();
  readers--;
  if (readers == 0)
    unlocked.signal_all();
  mutex.unlock();
}

write_lock() {
  mutex.lock();
  while (writer || (readers > 0))
    unlocked.wait(mutex);
  writer = true;
  mutex.unlock();
}

write_unlock() {
  mutex.lock();
  writer = false;
  unlocked.signal_all();
  mutex.unlock();
}

Cette mise en œuvre présente cependant quelques inconvénients.

Réveille tous les serveurs chaque fois que la serrure devient disponible

Si la plupart des serveurs attendent un verrou en écriture, c'est inutile - la plupart des serveurs ne parviendront pas à acquérir le verrou, après tout, et recommenceront à attendre. La simple utilisation de signal() ne fonctionne pas, car vous voulez réveiller tout le monde en attente d'un déverrouillage du verrou en lecture. Donc, pour résoudre ce problème, vous avez besoin de variables de condition distinctes pour la lisibilité et l'écriture.

Pas d'équité. Les lecteurs affament les écrivains

Vous pouvez résoudre ce problème en suivant le nombre de verrous en lecture et en écriture en attente, et soit arrêter d'acquérir des verrous en lecture une fois qu'il y a des verrous en écriture en attente (bien que vous affamerez ensuite les lecteurs!), Soit en réveillant au hasard tous les lecteurs ou un écrivain (en supposant vous utilisez une variable de condition distincte, voir la section ci-dessus).

Les serrures ne sont pas distribuées dans l'ordre où elles sont demandées

Pour garantir cela, vous aurez besoin d'une vraie file d'attente. Vous pourriez par exemple créez une variable de condition pour chaque serveur et signalez tous les lecteurs ou un seul écrivain, tous deux en tête de file d'attente, après avoir relâché le verrou.

Même les charges de travail en lecture pure provoquent des conflits en raison du mutex

Celui-ci est difficile à réparer. Une façon consiste à utiliser des instructions atomiques pour acquérir des verrous de lecture ou d'écriture (généralement comparer et échanger). Si l'acquisition échoue parce que le verrou est pris, vous devrez vous replier sur le mutex. Faire cela correctement est cependant assez difficile. De plus, il y aura toujours des conflits - les instructions atomiques sont loin d'être gratuites, en particulier sur les machines avec beaucoup de cœurs.

Conclusion

L'implémentation correcte des primitives de synchronisation est difficile. L'implémentation de primitives de synchronisation efficaces et équitables est pairplus difficile. Et cela ne rapporte presque jamais. pthreads sur linux, par ex. contient un verrou de lecture/écriture qui utilise une combinaison de futex et d'instructions atomiques, et qui surpasse donc probablement tout ce que vous pouvez trouver en quelques jours de travail.

45
fgp

Cochez cette classe :

//
// Multi-reader Single-writer concurrency base class for Win32
//
// (c) 1999-2003 by Glenn Slayden ([email protected])
//
//


#include "windows.h"

class MultiReaderSingleWriter
{
private:
    CRITICAL_SECTION m_csWrite;
    CRITICAL_SECTION m_csReaderCount;
    long m_cReaders;
    HANDLE m_hevReadersCleared;

public:
    MultiReaderSingleWriter()
    {
        m_cReaders = 0;
        InitializeCriticalSection(&m_csWrite);
        InitializeCriticalSection(&m_csReaderCount);
        m_hevReadersCleared = CreateEvent(NULL,TRUE,TRUE,NULL);
    }

    ~MultiReaderSingleWriter()
    {
        WaitForSingleObject(m_hevReadersCleared,INFINITE);
        CloseHandle(m_hevReadersCleared);
        DeleteCriticalSection(&m_csWrite);
        DeleteCriticalSection(&m_csReaderCount);
    }


    void EnterReader(void)
    {
        EnterCriticalSection(&m_csWrite);
        EnterCriticalSection(&m_csReaderCount);
        if (++m_cReaders == 1)
            ResetEvent(m_hevReadersCleared);
        LeaveCriticalSection(&m_csReaderCount);
        LeaveCriticalSection(&m_csWrite);
    }

    void LeaveReader(void)
    {
        EnterCriticalSection(&m_csReaderCount);
        if (--m_cReaders == 0)
            SetEvent(m_hevReadersCleared);
        LeaveCriticalSection(&m_csReaderCount);
    }

    void EnterWriter(void)
    {
        EnterCriticalSection(&m_csWrite);
        WaitForSingleObject(m_hevReadersCleared,INFINITE);
    }

    void LeaveWriter(void)
    {
        LeaveCriticalSection(&m_csWrite);
    }
};

Je n'ai pas eu l'occasion de l'essayer, mais le code semble correct.

6
Khachatur