Cet article couvre les bases du threading (multitâche) dans le Small Basic. Il s'agit généralement d'un concept avancé et il peut paraître surprenant de parler de ce sujet avec le Small Basic, mais en réalité il pointe le bout de son nez dans bien des programmes Small Basic. Par conséquent, il convient d'expliquer ce que c'est, et comment et pourquoi éviter certains pièges classiques.

Note du traducteur: Le terme de threading est souvent apparenté à la notion de multitâche qui est ce qui s'en rapproche le plus. Dans cet article, a défaut d'avoir de meilleur terme français, nous garderons les termes anglais. N'hésitez pas à laisser des commentaires ou de corriger l'article si vous avez une bonne traduction à proposer.

Que sont les Threads?

Un thread est processus (fil d'exécution de code) dans un programme qui s'exécute en même temps que d'autres processus ou threads. On parle d'"asynchrone" étant donné que plusieurs threads s'exécutent en même temps.

Les threads peuvent s'exécuter sur des CPUs (microprocesseurs) séparés si vous avez un PC Multi-Cores ou bien partagés sur un CPU. Le système d'exploitation se charge de tout celà pour vous, mais vous devez garder à l'esprit qu'ils s'exécutent tous en même temps.

Le "Thread UI" ou Thread d'Interface Utilisateur

Le thread principal qui démarre lorsqu'un programme Small Basic démarre et qui est appelé le Thread UI (User Interface - Interface Utilisateur en français). Il est responsable d'exécuter le programme principal (tout sauf les sous-routines d'événements). Il a également la charge de mettre à jour ce que vous voyez (l'Interface Utilisateur), par exemple la fenêtre GraphicsWindow. Lorsque vous appelez une commande comme Shapes.AddEllipse(20,20), l'objet ellipse et ces propriétés sont créés et un message est transmis au Thread UI pour qu'il mette à jour ce que nous voyons à l'écran.

Threads d'Evénement

Lorsqu'un événement est défini dans Small Basic, c'est événement est associé à une sous-routine d'événement. Cette sous-routine est appelée dans un nouveau thread lorsque l'événement est provoqué. En clair, un événement comme un clic souris peut avoir lieu n'importe quand et la sous-routine d'événement est appelée de manière asynchrone - en parallèle ou en même temps que le programme principal ou d'autres threads d'événement.

Threads Multiples

Le programme démarre toujours dans le thread principal, et des threads supplémentaires sont créés dans Small Basic lorsque les sous-routines d'événement sont appelés. Lorsqu'une sous-routine d'événement se termine, alors le thread d'événement est libéré. Il y a donc une grande distinction entre le code qui s'exécute sur le thread principal et le code appelée depuis les événements.

Ce ne sont pas les sous-routines qui déterminent sur quel thread elles s'exécutent, mais plutôt comment elles sont appelées (depuis le programme principal ou depuis un événement). Par conséquent, il est possible d'appeler une sous-routine d'événement (c'est juste une sous-routine) depuis le thread principal et on peut également appeler une sous-routine normale (pas une sous-routine d'événement) depuis un événement.

RingBell()
GraphicsWindow.MouseDown = OnMouseDown
 
Sub OnMouseDown
  RingBell()
EndSub
 
Sub RingBell
  Sound.PlayBellRing()
EndSub

Dans le code précédent, OnMouseDown est un événement de sous-routine et RingBell est une sous-routien 'normale'.  RingBell est appelé au début du thread principal et est également appelé par OnMouseDown dans le thread d'événement à chaque fois que le bouton de la souris est cliqué. Donc Sound.PlayBellRing() est appelé soit sur le thread principal soit sur un thread événement, cela dépend uniquement de comment elle est appelée.

Quels sont les problèmes avec les threads dans Small Basic?

Cette section récapitule quelques principaux problèmes avec les threads que l'on rencontrent dans tous les programmes multitâches et en particulier dans Small Basic.  Nous allons également tirer des conclusions sur les 'bonnes pratiques' découlant de ces questions.

Comme les problèmes relatifs au multitâche peuvent provoquer des comportements inattendus, leur débogage et résolution est particulièrement difficile, par conséquent il est préférable de concevoir votre code an gardant ces notions à l'esprit.

Données Globales

Toutes les variables Small Basic ont une portée globale. Cela implique qu'elles peuvent être accessibles et modifiées depuis n'importe quel point d'un programme Small Basic. Il en résulte, que si différents threads utilisent la même variable pour faire différentes choses en même temps, alors des résultats inattendus peuvent avoir lieu. Soyons particulièrement attentifs de ne pas utiliser les mêmes index de boucles à compteur (variables utilisées dans les boucle For)dans le code principal et les sous-routines d'événements.

Timer.Interval = 1000
Timer.Tick = OnTick
 
'Thread UI principal
i = 0
While ("True")
  i = i+1
  TextWindow.WriteLine("UI Thread "+i)
  Program.Delay(1000)
EndWhile
 
'Thread de l'événement Timer
Sub OnTick
  For i = 1 To 10
    For j = 1 To 1000
      y[j] = i*j
    EndFor
  EndFor
EndSub

Ci-dessus un exemple plutôt tiré par les cheveux, mais qui montre le problème que pose l'utilisation d'une variable 'i' dans le thread UI ainsi que dans les threads Timer et par conséquent sa valeur qui est modifiée par les deux donnant des résultats imprévisible comme par exemple ci-dessous.

Conclusion - N'utilisez pas la même variable dans un code pouvant être appelé depuis différents threads, sauf si vous voulez qu'elle soit modifiée et utilisée dans ce context, et dans ce cas il n'y a aucune garantie de savoir exactement que les variables seront modifiées. Dans Small Basic les variables 'thread safe' (protégées dans un environnement multitâche) par conséquent votre programme ne peut pas 'planter', mais vous pouvez avoir des comportements inattendus.

Réentrance

La réentrance c'est quand un thread d'événement est appelé une nouvelle fois avant qu'il n'est terminé l'événement précédent et quitté la sous-routine d'événement. La plupart des événements Small Basic ne sont pas ré-entrants. L'exemple suivant appelle le son de cloche à chaque fois que la souris est déplacée et ne rappelle par l'événement tant qu'il n'a pas terminé le dernier événement. Regardez comment le débogage affiché dans TextWindow affiche que nous "Entrons" et "Quittons" la sous-routine d'événement dans l'ordre (pas de réentrance). A noter que lorsque la souris bouge plusieurs événements sont provoqués chaque mouvement, mais seul le dernier événement est gardé pour la sous-routine d'événement, ce qui provoque un son de cloche supplémentaire après que vous ayez fini de bouger la souris.

GraphicsWindow.MouseMove = OnMouseMove
 
Sub OnMouseMove
  TextWindow.WriteLine("Nous Entrons")
  Sound.PlayBellRingAndWait()
  TextWindow.WriteLine("Nous Quittons")
EndSub

Notez cette fois, l'utilisation de Timer, qui est ré-entrant, plusieurs fois avant que l'événement ne se termine.

Timer.Interval = 100
Timer.Tick = OnTick
 
Sub OnTick
  TextWindow.WriteLine("Nous Entrons")
  Sound.PlayBellRingAndWait()
  TextWindow.WriteLine("Nous Quittons")
EndSub

Si nous arrêtons le Timer après 2 appels, nous pouvons constater qu'il y a plusieurs sons de cloche entendus (environ 20) qui sont provoqués avant que le son en cours ne se termine. Dans ce cas, les appels d'événement Timer sont empilés pour être appelés avant que le timer se soit mis en pause.

Timer.Interval = 100
Timer.Tick = OnTick
count = 0
 
Sub OnTick
  count = count+1
  If (count > 2) Then
    Timer.Pause()
  EndIf
  TextWindow.WriteLine("Nous Entrons "+count)
  Sound.PlayBellRingAndWait()
  TextWindow.WriteLine("Nous Quittons "+count)
EndSub

Conclusion - La réentrance est complexe et peut être imprévisible. N'en faîtes pas plus dans le code d'un événement qui pourrait durer plus longtemps à s'exécuter que la fréquence à laquelle l'événement peut être appelé. La meilleure approche est de définir un indicateur dans la sous-routine d'événement et de 'faire le travail' dans le thread principal. 

Voici un exemple de cette technique pour l'événement Timer avec des résultats plus prévisibles.

count = 0
timerTick = "False"
Timer.Interval = 100
Timer.Tick = OnTick
 
While ("True")
  If (timerTick) Then
    timerTick = "False"
    count = count+1
    If (count > 2) Then
      Timer.Pause()
    EndIf
    TextWindow.WriteLine("Nous Entrons "+count)
    Sound.PlayBellRingAndWait()
    TextWindow.WriteLine("Nous Quittons "+count)
  EndIf
  Program.Delay(20)
EndWhile
 
Sub OnTick
  timerTick = "True"
EndSub

Latence de l'Interface

Comme indiqué précédemment, le thread principal se charge également de l'Interface (UI). Si nous provoquons beaucoup de travail de mise à jour dans un thread d'événement qui prend un certain temps, l'Interface ne sera pas modifiée tant que la sous-routine d'événement ne soit terminée.

GraphicsWindow.MouseMove = OnMouseMove
 
Sub OnMouseMove
  TextWindow.WriteLine("Nous Entrons")
  ball = Shapes.AddEllipse(50,50)
  Shapes.Move(ball,GraphicsWindow.MouseX-25,GraphicsWindow.MouseY-25)
  Sound.PlayBellRingAndWait()
  TextWindow.WriteLine("Nous Sortons")
EndSub

Dans cet exemple la balle n'est pas dessinée tant que la sous-routine d'événement ne soit terminée. Comme la sous-routine n'est pas ré-entrante (voir le débogage dans la TextWindows), les événements de mouvement de souris sont empilés et continuent d'être appelés longtemps après que nous ayons fini de bouger la souris. Cela etst dû au fait que l'Interface est mise à jour lorsque la dernière balle est créée et le nouveau mouvement de souris peut modifier quelque chose, donc l'événement est empilé pour être appelé.

Si nous supprimons le son de cloche (ajouté pour ralentir la sous-routine d'événement) pour obtenons généralement un plantage et la TextWindow de débogage nous montre qu'il peut y avoir de la réentrance, peut-être que la variable de la balle est remplacée avant que nous ayons terminé avec. Il n'y a pas de solution simple dans ce cas si nous gérons le graphisme (travail d'interface) dans la sous-routine d'événement.

Conclusion - Ne faîtes pas de modification d'Interface dans une sous-routine d'événement, faîtes les dans le thread UI principal, comme ci-dessous. 

mouseMove = "False"
GraphicsWindow.MouseMove = OnMouseMove
 
While ("True")
  If (mouseMove) Then
    mouseMove = "False"
    ball = Shapes.AddEllipse(50,50)
    Shapes.Move(ball,GraphicsWindow.MouseX-25,GraphicsWindow.MouseY-25)
  EndIf
  Program.Delay(20)
EndWhile
 
Sub OnMouseMove
  mouseMove = "True"
EndSub

A noter que créer des centaines de formes de balles à chaque mouvement de souris est une très mauvaise idée, nous le faisons uniquement pour créer des exemples rapides.

Thread de Timer

Comme technique avancée nous pouvons généralement utiliser le thread Timer pour délibérément exécuter quelque chose qui prends beaucoup de temps, tandis que nous faisons des choses normales dans le thread principal d'UI. Tenez bien compte des conclusions précédentes:

  1. Faîtes attention à ne pas utiliser n'importe qu'elle variable comme les compteurs de boucle qui sont également utilisée dans le thread du programme principal.
  2. Lorsque la sous-routine de l'événément timer est appelée, arrêter immédiatement le timer pour ne pas provoquer de réentrance, vous pouvez également utiliser un long intervalle pour être sur de cela.
  3. Ne faîtes aucun travail graphique dans ce thread.

Comme exemple, on utilise un thread Timer pour charger des images que l'on va utiliser dans un affichage d'image qui défile - LitDev (l'auteur) a proposé cet exemple comme solution à un défi mensuel à la lumière de différentes discussion concernant le multitâche et de l'objet Timer dans le forum. C'est la raison principale de l'écriture de cet article. Vous pouvez importer le code dans Small Basic : WFD474.

'Example solution to December 2013 Challenge (Curriculum 2.6(3))
'Uses Timer and Stack to asynchronously download images in preparation for use
'Shape images are created and destroyed as required
'The only limiting feature is that ImageList will continue to grow gradually consuming memory
 
'Setup variables and GW
Initialise()
 
'Start Timer to get images asynchronously (in parallel on Timer thread)
Timer.Interval = 1000
Timer.Tick = OnTick
 
'Main loop
While "True"
  start = Clock.ElapsedMilliseconds
  
  UpdateImages()
  
  'Pause up to 10ms depending on time spent doing updates
  delay = 10 - (Clock.ElapsedMilliseconds-start)
  If (delay > 0) Then
    Program.Delay(delay)
  EndIf
EndWhile
 
'===========================================================
'SUBROUTINES
'===========================================================
 
'All initialisation
Sub Initialise
  imageCount = 0 'Current image number
  imageShape = "" 'Store for current moving images
  tag = "santa claus" 'Tag for Flickr image
  
  'Setup GW
  gw = Desktop.Width-40
  gh = 600
  GraphicsWindow.BackgroundColor = "Black"
  GraphicsWindow.Top = 10
  GraphicsWindow.Left = 10
  GraphicsWindow.Title = tag+" slideshow"
  GraphicsWindow.Height = gh
  GraphicsWindow.Width = gw
  GraphicsWindow.BrushColor = "Pink"
  GraphicsWindow.FontSize = 50
  loading = Shapes.AddText("loading...")
  Shapes.Move(loading,100,100)
  Mouse.HideCursor()
EndSub
 
'Asynchronous download of images - careful not to use any variable names used in main loop subroutines
Sub OnTick
  Timer.Pause() ' Stop the timer as this sub will continue for ever loading images as required
  While ("True")
    If (Stack.GetCount("images") < 10) Then 'load up to 10 images ready to use
      'If we don't use ImageList then we cannot get the image size
      'There is no way in standard SB to remove used images from ImageList so memory will gradually grow
      image = ImageList.LoadImage(Flickr.GetRandomPicture(tag))
      w = ImageList.GetWidthOfImage(image)
      h = ImageList.GetHeightOfImage(image)
      img = Shapes.AddImage(image) 'We can get some flicker when image is added - to minimise we immediately move off-screen
      Shapes.Move(img,-w,(gh-h)/2)
      Stack.PushValue("images", img)
      If (imageShape = "") Then 'Create first image
        NextImage()
        Shapes.Remove(loading)
      EndIf
    EndIf
    Program.Delay(10) 'Don't mash cpu when nothing to do
  EndWhile
EndSub
 
'Get the next image and add it to the moving list array
Sub NextImage
  If (Stack.GetCount("images") > 0) Then
    imageCount = imageCount+1
    imageShape[imageCount] = Stack.PopValue("images")
  EndIf
EndSub
 
'Move the current active images
Sub UpdateImages
  indexShapes = Array.GetAllIndices(imageShape)
  For i = 1 To Array.GetItemCount(indexShapes)
    'Get current moving image and update its position
    index = indexShapes[i]
    shape = imageShape[index]
    x = Shapes.GetLeft(shape)
    y = Shapes.GetTop(shape)
    Shapes.Move(shape, x+1, y)
    'Start a new image (with a pixel space of 10)
    If (x < 10 And x+1 >= 10) Then
      NextImage()
    EndIf
    'Clean up and remove finished images
    If (x > gw) Then
      Shapes.Remove(shape) 'Remove finished with shape
      imageShape[index] = ""
    EndIf
  EndFor
EndSub

Voir Aussi


Autres Languages