Attention: cette page se réfère à une ancienne version de SFML. Cliquez ici pour passer à la dernière version.

Utiliser les threads et les mutexs

Introduction

Tout d'abord, il faut dire que ce tutoriel, bien qu'étant le premier, n'est pas le plus important. En fait vous pouvez très bien commencer à apprendre et à utiliser la SFML sans thread ni mutex. Mais en tant que module de base pour tous les autres modules, il est important de savoir quelles fonctionnalités il peut offrir.

Qu'est-ce qu'un thread ?

Avant d'entrer dans le vif du sujet, qu'est-ce qu'un thread ? Dans quel cas doit-on les utiliser ?

Basiquement, un thread est un moyen d'exécuter un ensemble d'instructions indépendamment du thread principal (tout programme tourne dans un thread : il s'agit de programmes monothreaded ; lorsque vous ajoutez un ou plusieurs threads, il s'agit alors de programmes multithreaded). Démarrer un thread, c'est un peu comme exécuter un nouveau processus, excepté que c'est beaucoup plus léger -- les threads sont aussi appelés processus légers. Tous les threads partagent les données de leur processus parent. Ainsi, vous pouvez avoir deux ou plusieurs ensembles d'instructions s'exécutant en parallèle au sein de votre programme.
Ok, alors quand faut-il utiliser les threads ? En gros, lorsque vous devez exécuter quelque chose demandant beaucoup de temps (un calcul complexe, attente d'un évènement, ...) tout en évitant de bloquer l'exécution du programme. Par exemple, vous pourriez vouloir afficher une interface graphique pendant que vous chargez les ressources de votre jeu (ce qui prend habituellement pas mal de temps), ainsi vous pouvez placer le code de chargement dans un thread séparé du reste. Les threads sont également largement utilisés dans la programmation réseau, afin d'attendre que des données soient réceptionnées tout en continuant l'exécution du programme.

Dans la SFML, les threads sont définis par la classe sf::Thread. Il existe deux façons de l'utiliser, selon vos besoins.

Utilisation de sf::Thread avec une fonction de rappel (callback)

La première manière d'utiliser sf::Thread est de lui fournir une fonction à exécuter. Lorsque vous démarrez le thread, cette fonction sera appelée en tant que point d'entrée du thread. Celui-ci prendra fin automatiquement lorsque la fonction se terminera.

Voici un exemple simple :

#include <SFML/System.hpp>
#include <iostream>

void ThreadFunction(void* UserData)
{
    // Afficher quelque chose...
    for (int i = 0; i < 10; ++i)
        std::cout << "Je suis le thread numero 1" << std::endl;
}

int main()
{
    // Création d'un thread avec notre fonction
    sf::Thread Thread(&ThreadFunction);

    // Lancement du thread !
    Thread.Launch();

    // Afficher quelque chose...
    for (int i = 0; i < 10; ++i)
        std::cout << "Je suis le thread principal" << std::endl;

    return EXIT_SUCCESS;
}

Lorsque Thread.Launch() est appelé, l'exécution est séparée en deux flots indépendants : pendant que ThreadFunction() s'exécute, main() est en train de se terminer. Ainsi le texte des deux threads sera affiché en même temps.

Soyez vigilants : ici la fonction main() peut très bien se terminer avant la fin de ThreadFunction(), terminant ainsi le programme alors que le thread est toujours actif. Cependant ce n'est pas le cas : le destructeur de sf::Thread va attendre que le thread se termine, et ainsi s'assurer que le thread ne vivra pas plus longtemps que l'instance de sf::Thread qui le possède.

Si vous avez des données à passer au thread, vous pouvez les passer au constructeur de sf::Thread :

MyClass Object;
sf::Thread Thread(&ThreadFunction, &Object);

Vous pouvez ensuite les récupérer avec le paramètre UserData :

void ThreadFunction(void* UserData)
{
    // Transtypage du paramètre en son type réel
    MyClass* Object = static_cast<MyClass*>(UserData);
}

Attention, assurez-vous que les données que vous passez ne seront pas détruites avant que le thread en ait fini avec !

Utilisation de sf::Thread en tant que classe de base

L'utilisation d'une fonction callback en tant que thread est simple, et peut être utile dans certains cas, mais ne se révèle pas très flexible. En effet, que se passe-t-il si vous voulez utiliser un thread au sein d'une classe ? Ou si vous voulez partager plus de variables entre votre thread et le thread principal ?

Afin de permettre plus de flexibilité, sf::Thread peut également être utilisée en tant que classe de base. Au lieu de lui passer une fonction callback à la construction, vous pouvez en dériver et redéfinir la fonction virtuelle Run(). Voici le même exemple que ci-dessus, écrit avec une classe dérivée au lieu de la fonction callback :

#include <SFML/System.hpp>
#include <iostream>

class MyThread : public sf::Thread
{
private :

    virtual void Run()
    {
        // Afficher quelque chose...
        for (int i = 0; i < 10; ++i)
            std::cout << "Je suis le thread numero 2" << std::endl;
    }
};

int main()
{
    // Création d'une instance de notre classe perso de thread
    MyThread Thread;

    // On lance le thread !
    Thread.Launch();

    // Afficher quelque chose...
    for (int i = 0; i < 10; ++i)
        std::cout << "Je suis le thread principal" << std::endl;

    return EXIT_SUCCESS;
}

Si utiliser un nouveau thread est une fonctionnalité interne qui ne doit pas apparaître dans l'interface publique de votre classe, vous pouvez utiliser l'héritage privé :

class MyClass : private sf::Thread
{
public :

    void DoSomething()
    {
        Launch();
    }

private :

    virtual void Run()
    {
        // Quelque chose...
    }
};

Ainsi, votre classe n'exposera pas inutilement les fonctions liées à la manipulation de thread dans son interface publique.

Stopper un thread

Comme nous l'avons déjà mentionné, un thread prend fin lorsque la fonction associée se termine. Mais comment faire pour terminer un thread à partir d'un autre thread, sans attendre que sa fonction prenne fin ?

Il existe deux façons de terminer un thread, mais une seule est réellement sûre. Regardons d'abord la manière brutale et non sûre de le faire :

#include <SFML/System.hpp>

void ThreadFunction(void* UserData)
{
    // Quelque chose...
}

int main()
{
    // Créons un thread avec notre fonction
    sf::Thread Thread(&ThreadFunction);

    // Démarrons le !
    Thread.Launch();

    // Pour une raison X, nous voulons le stopper immédiatement
    Thread.Terminate();

    return EXIT_SUCCESS;
}

En appelant Terminate(), le thread sera immédiatemment stoppé sans aucune chance de terminer son exécution proprement. Cela s'avère même pire, car selon votre système d'exploitation, le thread peut même ne pas libérer les ressources qu'il détient.
Voici ce que dit la MSDN à propos de la fonction TerminateThread pour Windows : TerminateThread est une fonction dangereuse qui ne devrait être utilisée que dans les cas les plus extrêmes.

Le seul moyen sûr de terminer un thread actif est de simplement attendre qu'il se finisse de lui-même. Pour attendre la fin d'un thread, vous pouvez utiliser sa fonction Wait() :

Thread.Wait();

Ainsi, pour ordonner à un thread de se terminer, vous pouvez lui envoyer un évènement, utiliser une variable booléene, ou ce que vous voulez. Puis attendre qu'il se termine tout seul. Voici un exemple :

#include <SFML/System.hpp>

bool ThreadRunning;

void ThreadFunction(void* UserData)
{
    ThreadRunning = true;
    while (ThreadRunning)
    {
        // Quelque chose...
    }
}

int main()
{
    // Créons un thread avec notre fonction
    sf::Thread Thread(&ThreadFunction);

    // Démarrons le !
    Thread.Launch();

    // Pour une raison X, nous voulons le stopper immédiatement
    ThreadRunning = false;
    Thread.Wait();

    return EXIT_SUCCESS;
}

C'est plutôt simple, et beaucoup plus sûr que d'appeler Terminate().

Endormir un thread

Qu'il s'agisse du thread principal ou d'un thread que vous avez créé, il vous est possible de le mettre en pause pour une durée donnée grâce à la fonction sf::Sleep :

sf::Sleep(0.1f);

Comme toute durée manipulée par la SFML, le paramètre est un flottant définissant le temps de pause en secondes. Ici ce sera donc une pause de 100 ms.

Utiliser un mutex

Ok, commençons par définir ce qu'est un mutex. Un mutex (MUTual EXclusion) est une primitive de synchronisation. Ce n'est pas la seule : il existe les sémaphores, les sections critiques, les conditions, etc. Mais la plus importante est le mutex. En gros, vous l'utilisez pour verrouiller un morceau de code spécifique qui ne doit pas être interrompu, pour empêcher les autres threads d'y accéder. Lorsqu'un mutex est verrouillé, tout autre thread tentant de le verrouiller à son tour sera mis en attente jusqu'à ce que le thread qui l'a verrouillé le premier le déverrouille. Typiquement, les mutexs sont utilisés pour contrôler l'accès aux variables qui sont partagées entre plusieurs threads.

Si vous exécutez les exemples de code donnés au début de ce tutoriel, vous verrez que le résultat n'est peut-être pas ce à quoi vous vous attendiez : les textes des threads sont complétement mélangés. Ceci est dû au fait que les threads accèdent à la même ressource en même temps (ici la sortie standard). Pour éviter ceci, on peut utiliser un sf::Mutex :

#include <SFML/System.hpp>
#include <iostream>

sf::Mutex GlobalMutex; // Ce mutex servira à synchroniser nos threads

class MyThread : public sf::Thread
{
private :

    virtual void Run()
    {
        // On verrouille le mutex, pour s'assurer qu'aucun autre thread ne nous interrompera
        // pendant que nous affichons sur la sortie standard
        GlobalMutex.Lock();

        // On affiche quelque chose...
        for (int i = 0; i < 10; ++i)
            std::cout << "Je suis le thread numero 2" << std::endl;

        // On déverrouille le mutex
        GlobalMutex.Unlock();
    }
};

int main()
{
    // Créons une instance de notre classe perso
    MyThread Thread;

    // Démarrons le thread !
    Thread.Launch();

    // On verrouille le mutex, pour s'assurer qu'aucun autre thread ne nous interrompera
    // pendant que nous affichons sur la sortie standard
    GlobalMutex.Lock();

    // On affiche quelque chose...
    for (int i = 0; i < 10; ++i)
        std::cout << "Je suis le thread principal" << std::endl;

    // On déverrouille le mutex
    GlobalMutex.Unlock();

    return EXIT_SUCCESS;
}

Si vous vous demandez pourquoi nous n'utilisons pas de mutex pour accéder à la variable ThreadRunning dans l'exemple plus haut (pour stopper un thread), voici les deux raisons :

Soyez vigilants lorsque vous utilisez un mutex ou toute autre primitive de synchronisation : si vous ne les utilisez pas avec précaution, ils peuvent mener à des interblocages (deadlocks -- plusieurs threads verrouillés qui attendent les uns sur les autres indéfiniment).

Mais il reste un problème : que se passe-t-il si une exception est levée alors qu'un mutex est verrouillé ? Le thread n'aura aucun moyen de le déverrouiller, ce qui causera probablement un interblocage et gèlera votre programme. Afin de gérer ce problème correctement sans avoir à écrire des tonnes de code supplémentaire, la SFML fournit une classe sf::Lock.

Une version plus sûre du code ci-dessus serait donc la suivante :

{
    // On verrouille le mutex, pour s'assurer qu'aucun autre thread ne nous interrompera
    // pendant que nous affichons sur la sortie standard
    sf::Lock Lock(GlobalMutex);

    // On affiche quelque chose...
    for (int i = 0; i < 10; ++i)
        std::cout << "I'm the main thread" << std::endl;

}// Le mutex est déverrouillé automatiquement lorsque l'objet sf::Lock est détruit

Le mutex est déverrouillé dans le destructeur de sf::Lock, qui sera appelé même si une exception est levée.

Conclusion

Les threads et les mutexs sont très faciles à utiliser, mais soyez prudents : la programmation parallèle peut être beaucoup plus difficile qu'elle n'y paraît. Evitez les threads si vous n'en avez pas besoin, et essayez de partager aussi peu de variables que possibles entre plusieurs threads.

Vous pouvez maintenant retourner au sommaire des tutoriels, ou passer aux tutoriels du module de fenêtrage.