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.
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 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.
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.
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
EndSub
Sound
PlayBellRing
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.
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.
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
Tick
OnTick
'Thread UI principal
i
0
While
"True"
+
1
TextWindow
WriteLine
"UI Thread "
Program
Delay
EndWhile
'Thread de l'événement Timer
For
To
10
j
y
[
]
*
EndFor
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.
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.
MouseMove
OnMouseMove
"Nous Entrons"
PlayBellRingAndWait
"Nous Quittons"
Notez cette fois, l'utilisation de Timer, qui est ré-entrant, plusieurs fois avant que l'événement ne se termine.
100
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.
count
If
>
2
Then
Pause
EndIf
"Nous Entrons "
"Nous Quittons "
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.
timerTick
"False"
20
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.
ball
Shapes
AddEllipse
50
,
Move
MouseX
-
25
MouseY
"Nous Sortons"
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
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.
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:
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)
'Main loop
start
Clock
ElapsedMilliseconds
UpdateImages
'Pause up to 10ms depending on time spent doing updates
delay
'===========================================================
'SUBROUTINES
'All initialisation
imageCount
'Current image number
imageShape
""
'Store for current moving images
tag
"santa claus"
'Tag for Flickr image
'Setup GW
gw
Desktop
Width
40
gh
600
BackgroundColor
"Black"
Top
Left
Title
" slideshow"
Height
BrushColor
"Pink"
FontSize
loading
AddText
"loading..."
Mouse
HideCursor
'Asynchronous download of images - careful not to use any variable names used in main loop subroutines
' Stop the timer as this sub will continue for ever loading images as required
Stack
GetCount
"images"
<
'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
w
GetWidthOfImage
h
GetHeightOfImage
img
AddImage
'We can get some flicker when image is added - to minimise we immediately move off-screen
/
PushValue
'Create first image
NextImage
Remove
'Don't mash cpu when nothing to do
'Get the next image and add it to the moving list array
PopValue
'Move the current active images
indexShapes
Array
GetAllIndices
GetItemCount
'Get current moving image and update its position
index
shape
x
GetLeft
GetTop
'Start a new image (with a pixel space of 10)
And
'Clean up and remove finished images
'Remove finished with shape