samedi, avril 14, 2007

Synchronisation de threads

Les applications contenant plusieurs fils d'exécution indépendants doivent souvent synchroniser leurs opérations pour éviter les problèmes suivants:
  1. La mise à jour d'une variable partagée par au moins deux threads.
  2. L'attente de la disponibilité d'une ressource.
  3. L'exécution d'instructions suite à un événement.
Ces problèmes sont dûs au caractère non déterministe de l'ordonnancement de threads par le Système d'Exploitation.

Appliquer une solution

L'application oggreader (voir ancien post), devient gourmande en ressource processeur dès qu'on met le lecteur sur pause ou sur stop.
En fait ces deux fonctionnalités ont été mises en place en utilisant la technique du busy waiting qui consiste à boucler indéfiniment en attendant à ce qu'un événement quelconque se produise. Dans ce cas précis, le programme boucle jusqu'à ce que l'utilisateur se décide de cliquer sur play.

Ci-dessous les ressources utilisées lorsque l'application est lancée et que la lecture est en cours.


Lorsque l'on clic sur pause, on constate qu'il y a une utilisation anormalement forte du processeur.


Thread en attente

Il est possible de résoudre ce problème en mettant le thread Player en attente quand l'utilisateur clic sur pause ou stop. Lorsque la lecture est relancée par un clic sur play il suffit de réveiller le thread en attente. Bien que cette solution semble évidente à mettre en place, il faut prendre deux précautions, notamment:
  1. Ne pas perdre les messages de réveil du thread en l'envoyant alors que celui-ci n'est pas en encore en attente.
  2. Réveiller le thread si le programme est terminé alors que le lecteur est dans l'état pause ou stop.

Dans la classe Player on ajoute à la méthode play les lignes 9 à 11 qui permettent de réveiller inconditionnellement le thread mais qui met aussi à jour la variable is_waiting en une opération atomique. On aura pris soin d'initialiser cette variable au départ à 0 et d'avoir déclaré la variable condition de type QWaitCondition.

1 void Player::play() {
2 if (file == NULL) return;
3 if (is_playing || !is_ready) return;
4 thread_running = true;
5 is_playing = true;
6 if (!isRunning()) {
7 start();
8 } else {
9 QMutexLocker locker(&mutex);
10 condition.wakeOne();
11 is_waiting--;
12 }
13 return;
14 }

Toujours dans la même classe aux lignes 16-18, on incrémente is_playing et ensuite on teste si l'on peut mettre le thread en attente en vérifiant la valeur de la variable is_playing.

1 void Player::run() {
2 try {
3 while(thread_running) {
4 if (is_playing) {
5 // forward or rewind
6 if (offset_changed) {
7 decoder->seek(new_offset);
8 offset_changed = false;
9 pcm->drain();
10 }
11
12 if (!decoder->decode()) break;
13 pcm->write(decoder->get_buffer(), decoder->get_no_samples());
14 emit updated();
15 } else {
16 QMutexLocker locker(&mutex);
17 is_waiting++;
18 if (is_waiting != 0) condition.wait(&mutex);
19 }
20 }
21 } catch (GenericException &e) {
22 error_msg = e.what();
23 is_playing = false;
24 emit terminated();
25 }
26 pcm->drain();
27 decoder->seek(0);
28 is_playing = false;
29 }

Pour finir, il ne faut pas oublier de réveiller le thread avant que l'application ne termine.
1 void Player::stop_thread() {
2 thread_running = false;
3 condition.wakeOne();
4 }

Et le tour est joué. Le résultat est remarquable n'est-ce pas? Mais il y a toujours un petit chouia manquant, un genre de «faille» si vous voulez. A vous de découvrir ;o)