Synchronisation des threads

Une fois que vous êtes suffisamment avancés en codage “live” avec un certain nombre de fonctions et de threads en simultanéité, vous avez probablement remarqué que c’est plutôt facile de faire une faute dans l’un des threads et qui lui met fin. Ce n’est pas un gros problème car vous pouvez facilement redémarrer le thread en pressant Run. Toutefois, quand vous redémarrez le thread, il se trouve alors déphasé avec les threads d’origine.

Temps hérité

Comme nous en avons discuté auparavant, les nouveaux threads créés avec in_thread héritent de tous les paramètres du thread parent. Ceci inclut l’heure actuelle. Ce qui signifie que les threads sont toujours en phase entre eux quand ils sont démarrés simultanément.

Toutefois quand vous démarrez un thread tout seul, il démarre avec sa propre heure qui n’est probablement en phase avec aucun des autres threads en cours d’exécution.

Cue et sync

Sonic Pi fournit une solution à ce problème avec les fonctions cue et sync.

cue nous permet d’envoyer nos messages de battement de cœur à tous les autres threads. Par défaut, les autres threads ne sont pas intéressés et ignorent ces messages de battement de coeur. Cependant, vous pouvez leur déclarer de l’intérêt avec la fonction sync.

La chose importante à laquelle il faut être attentif est que sync est similaire à sleep dans le sens qu’elle arrête le thread courant et l’empêche de faire quoi que ce soit pendant un moment. Toutefois, avec sleep, vous spécifiez combien de temps vous voulez attendre tandis qu’avec sync, vous ne savez pas combien de temps vous allez attendre - étant donné que sync attend le cue suivant d’un autre thread, ce qui peut être court ou long.

Explorons ceci en un peu plus de détail :

in_thread do
  loop do
    cue :tick
    sleep 1
  end
end
in_thread do
  loop do
    sync :tick
    sample :drum_heavy_kick
  end
end

Nous avons ici deux threads - l’un agissant comme un métronome, ne jouant aucun son mais envoyant des messages de battement de cœur :tick à chaque temps. Le second thread se synchronise sur les messages tick et quand il en reçoit un, il hérite de l’heure du thread cue et continue de jouer.

Par conséquent, nous entendons l’échantillon :drum_heavy_kick exactement quand l’autre thread envoie le message :tick, même si les deux threads n’ont pas démarré leur exécution en même temps :

in_thread do
  loop do
    cue :tick
    sleep 1
  end
end
sleep(0.3)
in_thread do
  loop do
    sync :tick
    sample :drum_heavy_kick
  end
end

Ce vilain appel à sleep mettrait typiquement le second thread en déphasage avec le premier. Cependant, comme nous utilisons cue et sync, nous synchronisons automatiquement les threads en évitant un décalage de temps accidentel.

Noms des “cue”

Vous êtes libre d’utiliser n’importe quel nom que vous aimeriez pour vos messages cue, pas seulement :tick. Vous devez juste vous assurer que tout autre thread se synchronise sur le bon nom - autrement il attendrait à jamais (au moins jusqu’à ce que vous pressiez le bouton Stop).

Jouons avec quelques noms de cue :

in_thread do
  loop do 
    cue [:foo, :bar, :baz].choose
    sleep 0.5
  end
end
in_thread do
  loop do 
    sync :foo 
    sample :elec_beep
  end
end
in_thread do
  loop do
    sync :bar
    sample :elec_flip
  end
end
in_thread do
  loop do
    sync :baz
    sample :elec_blup
  end
end

Ici, nous avons une boucle principale cue qui envoie aléatoirement un des battements de cœur nommés :foo, :bar ou :baz. Nous avons aussi ensuite trois boucles en thread se synchronisant sur ces noms indépendamment et jouant un échantillon différent. Il est clair que nous entendons un son à chaque demi-temps puisque chacun des threads sync est aléatoirement synchronisé avec le thread cue et joue son échantillon.

Ceci, bien entendu, marche aussi si vous ordonnez les threads en sens inverse puisque les threads sync restent en attente du cue suivant.