IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Viadeo Twitter Facebook Share on Google+   
Logo Documentation Qt ·  Page d'accueil  ·  Toutes les classes  ·  Toutes les fonctions  ·  Vues d'ensemble  · 

Exemple de conditions d'attente

L'exemple de conditions d'attente montre comment utiliser QWaitCondition et QMutex pour contrôler l'accès à un tampon circulaire partagé par un thread producteur et un thread consommateur.

Le producteur écrit des données dans le tampon jusqu'à ce qu'il soit plein et ensuite recommence au début du tampon en écrasant les données existantes.

Les conditions d'attente rendent possible un niveau de concurrence plus élevé que ce qui est normalement possible avec uniquement des mutex. Si l'accès au tampon était simplement gardé par un QMutex, le thread consommateur ne pourrait pas accéder au tampon en même temps que le producteur, alors que pourtant il n'y a pas de danger si les deux threads écrivent au même moment dans des parties différentes du tampon.

L'exemple comprend deux classes : Producer et Consumer. Les deux héritent de QThread. Le tampon circulaire (buffer) utilisé pour la communication entre ces deux classes et les outils de synchronisation qui le protègent sont des variables globales.

Une alternative à l'utilisation de QWaitCondition et QMutex pour résoudre le problème producteur-consommateur est l'usage de QSemaphore. C'est ce que fait l'exemple Semaphores.

Variables globales

Commençons par étudier le tampon circulaire et les outils de synchronisation associés :

const int DataSize = 100000;
const int BufferSize = 8192;
char buffer[BufferSize];
 
QWaitCondition bufferNotEmpty;
QWaitCondition bufferNotFull;
QMutex mutex;
int numUsedBytes = 0;

DataSize est la quantité de données que le producteur va générer. Pour garder l'exemple aussi simple que possible, elle restera constante. BufferSize est la taille du tampon circulaire. Elle est inférieure à DataSize, ce qui veut dire qu'à un moment le producteur atteindra la fin du tampon et repartira du début.

Pour synchroniser le producteur et le consommateur, nous avons besoin de deux conditions d'attente et d'un mutex. La condition bufferNotEmpty est signalée lorsque le producteur a généré des données, avertissant le consommateur qu'il peut les lire. La condition bufferNotFull est signalée lorsque le consommateur a lu des données, avertissant le producteur qu'il peut en générer d'autres. numUsedBytes est le nombre d'octets dans le tampon de données.

Ensemble, les conditions d'attente, le mutex et le compteur numUsedBytes garantissent que le producteur n'est jamais plus de BufferSize octets en avance par rapport au consommateur et que ce dernier ne lit jamais des données que le producteur n'a pas encore généré.

Classe producteur

Étudions le code de la classe Producer :

class Producer : public QThread
{
public:
    void run();
};
 
void Producer::run()
{
    qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));
 
    for (int i = 0; i < DataSize; ++i) {
        mutex.lock();
        if (numUsedBytes == BufferSize)
            bufferNotFull.wait(&amp;mutex);
        mutex.unlock();
 
        buffer[i % BufferSize] = "ACGT"[(int)qrand() % 4];
 
        mutex.lock();
        ++numUsedBytes;
        bufferNotEmpty.wakeAll();
        mutex.unlock();
    }
}

Le producteur génère DataSize octets de données. Avant d'écrire un octet dans le tampon circulaire, il doit d'abord vérifier que le tampon n'est pas plein (c'est-à-dire numUsedBytes égal à BufferSize). Si le tampon est plein, le thread attend la condition bufferNotFull.

Finalement, le producteur incrémente numUsedBytes et signale que la condition bufferNotEmpty est vraie, puisque numUsedBytes est nécessairement supérieur à 0.

Nous gardons tous les accès à la variable numUsedBytes avec un mutex. De plus, la fonction QWaitCondition::wait() prend un mutex comme argument. Ce mutex est déverrouillé lorsque le thread s'endort et verrouillé lorsqu'il se réveille. De plus, la transition de l'état verrouillé à l'état en attente est atomique, pour prévenir une situation de compétition (race condition).

Classe consommateur

Regardons maintenant la classe Consumer :

class Consumer : public QThread
{
public:
    void run();
};
 
void Consumer::run()
{
    for (int i = 0; i < DataSize; ++i) {
        mutex.lock();
        if (numUsedBytes == 0)
            bufferNotEmpty.wait(&amp;mutex);
        mutex.unlock();
 
        fprintf(stderr, "%c", buffer[i % BufferSize]);
 
        mutex.lock();
        --numUsedBytes;
        bufferNotFull.wakeAll();
        mutex.unlock();
    }
    fprintf(stderr, "\n");
}

Le code est très similaire à celui du producteur. Avant de lire l'octet, nous vérifions que le tampon n'est pas vide (numUsedBytes égal 0) au lieu de vérifier qu'il n'est pas plein et attendons la condition bufferNotEmpty s'il est vide. Après avoir lu l'octet, nous décrémentons numUsedBytes (au lieu de l'incrémenter) et nous signalons la condition bufferNotFull (au lieu de la condition bufferNotEmpty).

Dans

main()

, nous créons les deux threads et appelons QThread::wait() pour garantir qu'ils auront le temps de se terminer avant la sortie de la fonction :

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);
    Producer producer;
    Consumer consumer;
    producer.start();
    consumer.start();
    producer.wait();
    consumer.wait();
    return 0;
}

Que se passe-t-il quand nous exécutons le programme ? Initialement, seul le producteur peut faire quelque chose ; le consommateur est bloqué sur l'attente de la condition bufferNotEmpty (numUsedBytes est à 0). Dès que le producteur a écrit un octet dans le tampon, numUsedBytes est égal à BufferSize - 1 et la conditionbufferNotEmpty est signalée. À cet instant, deux choses peuvent se produire : soit le consommateur prend la main et lit cet octet, soit le producteur peut en écrire un deuxième.

Le modèle producteur-consommateur présenté dans cet exemple rend possible l'écriture d'applications multithreads hautement concurrentes. Sur une machine multiprocesseurs, le programme est potentiellement jusqu'à deux fois plus rapide que l'équivalent basé sur un mutex, puisque les deux threads peuvent être actifs simultanément sur différentes parties du tampon.

Soyez cependant conscients que cet avantage n'est pas toujours réel. Verrouiller et déverrouiller un QMutex a un coût. En pratique, il serait probablement plus efficace de diviser le tampon en blocs et d'opérer sur ces blocs au lieu d'opérer sur des octets individuels. La taille du tampon est aussi un paramètre qui doit être choisi avec soin en se basant sur l'expérimentation.

Remerciements

Merci à Ilya Diallo pour la traduction et à Jonathan Courtois ainsi qu'à Jacques Thery pour leur relecture !

Cette page est une traduction d'une page de la documentation de Qt, écrite par Nokia Corporation and/or its subsidiary(-ies). Les éventuels problèmes résultant d'une mauvaise traduction ne sont pas imputables à Nokia. Qt 4.7
Copyright © 2024 Developpez LLC. Tous droits réservés Developpez LLC. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents et images sans l'autorisation expresse de Developpez LLC. Sinon, vous encourez selon la loi jusqu'à 3 ans de prison et jusqu'à 300 000 E de dommages et intérêts. Cette page est déposée à la SACD.
Vous avez déniché une erreur ? Un bug ? Une redirection cassée ? Ou tout autre problème, quel qu'il soit ? Ou bien vous désirez participer à ce projet de traduction ? N'hésitez pas à nous contacter ou par MP !