Frédéric Decréquy, version 4 du 23 juin 2018
1.TABLE DES MATIERES
1. TABLE
DES MATIERES
2. INTRODUCTION
3. CONTEXTE
4. LOCKS
DANS LES OBJETS DE TRAVAIL
5. DEAD
LOCKS
6. SINGULARISER
LES COMPORTEMENTS MULTITHREADS PAR TYPE DE COMPORTEMENT TECHNIQUE
7. AUTRES
OBJETS
8. COMMANDES
MULTITHREAD OU NON
9. COLLECTIONS
10. FAIRE
UN OBJET DE COLLECTION DE BASE HERITANT DE L’OBSERVABLECOLLECTION
11. ASYNC
AWAIT
12. WAITASYNC
13. PROBLEME
AVEC WAIT
14. BLOCS
D’ETAPES
15. TÂCHE
CONCURRENTE
16. LE
MINIMUM DE REQUÊTES SYNCHRONES
17. CONCLUSION
2.INTRODUCTION
Une adaptation majeure sans possibilité de
marche arrière doit être mise en place dans les projets modernes. Il ne faut
pas lésiner sur les méthodes d’assurance et de stabilisation.
Il s’agit du compartimentage des threads. En
WPF, il s’agit d’une séparation de la thread vidéo et de la thread
viewmodel. Nous sommes obligés d’activer cette séparation préconisée pour 2
raisons :
C’est la méthode préconisée par Microsoft
pour le concepteur d’interfaces WPF, car point d’action sur l’écran si la
thread vidéo est utilisée pour les traitements. Les utilisateurs nous
répètent inlassablement qu’ils voudraient que l’écran leur montrent que les
chargements sont en cours. La deuxième raison est que WPF est par principe
multithread. De nombreux éléments fonctionnent dans ce sens. Le binding, la
méthode la plus recommandée en WPF, ne fonctionne presque qu’en multithread.
De même que les commandes. En WinRT, il n’y a pas de fenêtres modales.
Il n’y a pas vraiment moyen de faire tourner
le ViewModel dans un seul thread, puisque le compartimentage bi-thread
revient presque au même que le compartimentage multithread. Il n’y a pas de
raison que les appels de la thread vidéo vers le viewmodel de la
bibliothèque basculent sur la thread du viewmodel puisque rien ne permet
d’assurer qu’il y en a qu’une spécifiquement à moins de le tester à chaque
entrée de la bibliothèque avec un code beaucoup plus gros que le simple
lock().
Les tests qui semblent concluants ne sont pas
parlants pour vérifier le compartimentage. Les méthodes de correction point
par point non plus. Avec les threads, on a l’impression que tout va bien
parce que le problème, c’est juste quand deux threads se rencontrent, mais
là où on les a laissé se rencontrer, ce peut être un cas qui arrive une fois
sur un million. Donc, il faut absolument faire un travail fini et propre
plutôt que de se laisser avoir par cette impression.
Partons donc sur une organisation cartésienne
même si cela prend du temps. Soyons organisés, et voyons large, le plus
large possible, afin de commencer par les actions les plus importantes, et
donc les plus encapsulant, quitte à réduire le nombre d’actions moins
importantes réalisées.
3.CONTEXTE
Ce n’est pas grave si deux codes à la fois
modifient la même valeur, si cette valeur est modifiée deux fois ce serait
qu’elle a soit une valeur soit une autre, les deux sont acceptables, mais le
problème, c’est lorsque des valeurs interdépendantes ne vont pas ensemble.
Particulièrement, quand on a commencé à parcourir une collection, il ne faut
plus qu’on la modifie. Les valeurs interdépendantes sont soit des reflets
d’une réalité, il faut alors réfléchir à pourquoi deux reflets sont en train
d’être installés alors qu’il ne s’est pas produit de changement d’état, et
particulièrement, lors d’un changement d’état, on ne doit plus être en train
de construire le précédent volet des choses.
Le truc, c’est donc d’utiliser des collections
sécurisées, mais aussi de rendre mono-thread toutes les interventions qui
ont lieues pendant l’application d’un reflet, ainsi que pendant le travail
d’un état. Ces travaux s’effectuent sur des classes à état. Il faut que
toutes les interventions sur une classe à état soient mono-thread. Donc
générer un objet de verrouillage par classe à état, privé ou protected pour
ne pas se le verrouiller à l’extérieur, verrouiller à chaque intervention,
donc dans presque chaque méthode de la classe, et des classes qui composent
avec.
4.LOCKS DANS LES OBJETS DE TRAVAIL
Action 1 : Le truc habituel, c’est de mettre des lock() à chaque
point d’entrée des classes du ViewModel.
On créerait un objet à verrouiller par classe,
privé afin d’être sûr qu’aucune communication de cette source de
verrouillage n’a lieu, et on le passerait à tous les sous-objets construits
autour de cette table.
Le lock() se fait sur cet objet, et ainsi, pour
toutes les instructions d’une classe, on est sûr de travailler sur la même
thread.
Car le lock() empêchera d’autres threads de
travailler avec cette classe pendant l’exécution d’instructions qui l’ont
verrouillé, ou empêchera la thread prévue
pour travailler avec cette classe de travailler avec alors qu’une autre est
en train d’y accéder. Un point intéressant et primordial est que le lock()
ne réalise un blocage que s’il se produit dans un thread différent du
premier lock sur l’objet verrouillé (uniquement prévu à cet effet et privé
dans la classe).
Point à se demander : Quelles sont les
instructions qui pourraient s’en passer ? Si toutes les instructions
publiques sont lockées, on peut s’en passer pour les instructions privées.
Protected aussi ? Oui puisque appelé uniquement par l’objet lui-même. Et les
obtentions de valeurs publiques ? Il faut locker si une intervention active
est possible. Et si aucune n’est prévue ? Le moment et la possibilité de
modification d’une valeur alors que celle-ci vient d’être lue est-elle
réellement anormale, du fait que de toute façon, elle aurait été modifiée
après sans avertir la thread appelante ? On peut se passer d’un lock.
Attention aux activités qui ont lieues en lisant d’autres propriétés à
l’intérieur de la propriété. Le truc est d’habitude d’écrire plutôt des
fonctions quand il y a une action potentielle, mais pas dans le cas de lazy
loading, un chargement sur besoin de la valeur, qui donc agit. Y réfléchir
quand le cas se présente afin de locker dans ce cas.
5.DEAD LOCKS
Autre point à se demander : Maintenant qu’il est
clair qu’il va falloir mettre en place un verrouillage normal, quels sont
les risques de Dead lock ? Les instructions liées à une classe verrouillent,
et peuvent être appelées par la thread d’un autre objet de la même classe
qui a verrouillé les siennes, puis le thread du premier objet de cette
classe peut appeler les instructions de ce second objet, verrouillées. Un
cas concret serait un chargement de valeurs suggérées, donc un second objet
composant du premier, le premier effectuant une opération en même temps, et
aurait besoin d’affecter quelque chose dans son objet de valeurs suggérées,
pendant que celui-ci avertit par un événement le premier objet que ses
valeurs suggérées sont chargées, provoquant un blocage. Ceci est tout à fait
possible.
Les solutions se trouvent dans la manière d’agir
enfant-parent : Le changement d’une propriété d’un parent n’est pas
totalement impossible, mais on peut détecter 2 façons de communiquer enfant
vers parent très générales et adaptables à part entière : Les enfants
communiquent généralement avec les parents par des événements.
Action 2 donc : On pourrait généraliser davantage afin de provoquer
des événements qui savent basculer d’un thread à l’autre en ce qui concerne
ceux qui ne nécessitent pas de synchronisation, il y en a.
Ça tombe bien, j’ai justement une bibliothèque
qui effectue des événements, spéciaux, qui permettent de ne pas retenir
l’appelé par l’écouté mais l’écouté par l’appelé, et on peut y insérer du
code à la fois pour basculer de thread, potentiellement vers un thread
connu, potentiellement pour locker aussi.
Deuxièmement, de nombreuses actions sont
simplement des changements qui font basculer l’état à inconnu, en attendant
un chargement par un lazyloading qui peut même être asynchrone, libérant
ainsi assez facilement le thread appelant. Sinon, par-ci par-là dans les
communications, certaines ne nécessitent pas de synchronisation et on peut
les basculer en transfert de thread, mais ce cas est très généralement la
spécialité des événements. Là, il n’y a rien à faire non plus.
Action 3 : De nombreuses actions sont des déchargements d’objets par
un positionnement dans l’état inconnu, tel des changements de filtre,
disais-je. Ce qu’il faut aussi faire, pour éviter au maximum les deadlock,
c’est de ne pas locker s’il s’agit d’un changement de propriété vers une
valeur identique. On peut aussi ne pas locker quand l’état est déjà inconnu.
Par contre, que faire quand il faut locker pour un petit travail, qui fait
passer à l’état inconnu alors qu’il est déjà inconnu ? La majorité des
deadllocks de ce minimum de cas se situent dans les événements provoqués par
le passage à l’état inconnu ou connu de l’objet. Il faudrait ne locker que
la partie qui effectue le petit travail, pas la partie qui vérifie que
l’objet est déjà à l’état inconnu. Cela dit, on peut faire en sorte que
l’événement indiquant l’état inconnu ne se réexécute que si le rechargement
a recommencé. On peut faire un transfert de thread pour l’événement de fin
de chargement, mais pas les autres, car il ne faut pas que les données
soient incohérentes, c’est là l’enjeu du problème. Il faut y réfléchir
événement par événement. Peut-on locker séparément le petit travail et le
calcul de déchargement ? D’autant qu’on peut ne pas locker l’événement
lui-même, qui n’agit pas dans son propre objet. Si on fait cette séparation,
il risque d’y avoir des threads qui vont reprendre juste avant le calcul du
déchargement. Il faut être sûr qu’il ne s’agit que d’un calcul de
déchargement, et puisqu’un déchargement de trop ne poserait pas de problème,
le rechargement étant toujours appelé si le besoin est là, on peut donc
faire cette séparation. Si le calcul de déchargement ou de rechargement fait
d’autres choses dans son objet, il faut conserver un lock unique.
Quatrièmement, le Dead lock aura d’autant moins
de chance d’être provoqué que le travail sera effectué généralement avec un
seul thread, la thread vidéo ayant été bloquée une unique première fois.
Cinquièmement, si on se rend compte que tout ne
tient pas dans de simples blocs lockés, mais qu’on veut faire des sorties
qui continuent à locker, ou protéger l’objet de lockage et pourtant le
distribuer, ou faire un lock avec une durée maximale, parce qu’un lock
devrait toujours être presque instantané…
Il y a possibilité de regrouper les activités de
lockage dans des fonctions, grâce à la fonction Enter qui imite le lock avec
la fonction correspondante de délockage, et il y a des moyens de s’assurer
d’effectuer la fonction correspondante, par exemple avec la fonction using,
qui peut être exécuté pour effectuer un Dispose qui compte ou qui libère,
sans invalider l’objet du using. Ce serait toujours les mêmes objets
utilisés dans les using. Pour toutes ces raisons, ce serait déjà une idée
d’avoir eu la prévoyance de faire le lock par une fonction.
6.SINGULARISER LES COMPORTEMENTS MULTITHREADS PAR TYPE DE
COMPORTEMENT TECHNIQUE
Sixièmement, il faudrait singulariser les
comportements multithreads par type de comportement technique.
· Faire
une fonction de tâche async pour la sauvegarde, le chargement et
l’initialisation qui charge, ainsi que toutes les fonctions qui y font appel
ou qui en sont la sous-partie centrale.
· On
ne peut pas faire de lock dans une fonction async. L’async est le principe
contraire, il faut essayer de mettre des fonctions async quand ce sont
plutôt des principes async qui s’y associent.
· Afin
d’éviter que les locks d’une propriété s’interlockent, il faudrait ne locker
que si on est sûr que la propriété va changer, et délocker avant le lock du
déchargement.
· Il
y a 5 phases dans la vie d’un objet à état :
o Les
fonctions de création de la structure purement réactive. Celles
qui ne sont appelées que dans cette phase ne seront pas appelées par
plusieurs threads. On peut éviter de gérer les locks.
o Les
propriétés. Les propriétés ne
peuvent pas être async. Pour imiter un principe de tâche, c’est facile, de
nombreuses valeurs cruciales pour les déchargements, par exemple les
filtres, peuvent être des instances de la classe valeur réactive, ou une
classe spécifiquement créée avec l’interface INotifyPropertyChanged, qui
permettent d’avoir un délégué de calcul, qui peut être async lui, donc
recevoir des sources de calculs async, même multiples, en gérant
spécifiquement les calculs concurrents qui ont été demandés, et une fonction
de changement de valeur, et on peut savoir quand la valeur est requise,
qu’elle n’est pas encore connue, grâce à un état interne, et un autre qui
indique que la valeur est demandée ou requise.
§ On
peut même gérer spécifiquement les calculs concurrents qui ont été demandés.
Dans ce cas, faire : Remplacer le délégué de calcul quand un nouveau calcul
async est demandé. Dans le cas de délégués avec lock, forcer le délockage
avant le nouveau lock, donc attendre la fin du calcul.
o La
sauvegarde, unaire, peut tourner par méthode async.
o Le
chargement, unaire, peut tourner par méthode async.
o Il
existe une écriture qui permet d’écrire des Tâches Async concurrentes,
pendant qu’elles s’exécutent. Les sous-parties non centrales peuvent
contenir les fameux locks.
o La
vie dans l’état chargé, mérite d’être géré par des fonctions Lock.
§ Façon
using, qui délocke au moment du Dispose, un singleton qui compte le nombre
de verrouillages, verrouillés par la fonction Enter au lieu d’un simple
Lock. Il permettrait de provoquer une erreur par timer au cas où un
verrouillage reste trop longtemps, puisque les locks ne sont pas sensés
s’exécuter avec des tâches asynchrones.
§ Tous
les Enter d’un objet et les objets qui lui appartiennent considèrent que
s’ils ont besoin d’un verrouillage, en ce qui concerne entre eux, il s’agit
de la même thread, donc un verrouillage sur un objet unique de verrouillage
par objet de travail. Particulièrement intéressant quand on sait qu’un
verrouillage ne bloque que les autres threads qui chercheraient ensuite à
bloquer aussi.
§ Attention,
il est bien que l’objet de verrouillage ne soit pas accessible à
l’extérieur, afin d’éviter de le verrouiller par d’autres threads. Donc le
singleton aussi.
§ Ceci
permet aussi de passer aux constructeurs des autres objets de travail
appartenant complètement à l’objet de travail principal, le singleton, qui
restera donc inaccessible des erreurs du développeur. Si l’objet n’est pas
construit dans le parent de cet arbre, on peut aussi faire une fonction de
renseignement du singleton dans le parent, qui vérifie s’il est bien le
parent.
7.AUTRES OBJETS
Action 4 : Gérer les locks des classes helpers annexes.
8.COMMANDES MULTITHREAD OU NON
Action 5 : Choisir intelligemment entre les commandes qui lancent du
multithread et ceux qui n’en lancent pas. Avoir une propriété indiquant le
type de lancement préconisé dans la commande.
9.COLLECTIONS
Action 6 : Dans le cas où les points d’entrée ne seront pas gérés par
le viewmodel, par exemple lors d’un affichage vidéo, il faut sécuriser les
collections en les basant sur des systèmes bloqués. Voir plus loin la
réécriture de l’ObservableCollection.
Action 7 : Revoir l’objet collection afin d’y intégrer des blocages
et des blocages dans l’énumération.
Action 8 : Revoir l’objet ObservableCollection, la collection
principale utilisée pour les lignes, qui gère l’observation, c’est-à-dire
l’envoi d’événements de modification. On pourrait remplacer cette
observation par autre chose, s’il s’agit juste de la synchroniser avec la
thread vidéo, il faut le faire pour après l’opération de remplissage, mais
ça peut faire des dead lock en revenant dans des threads viewmodel lors de
la lecture des valeurs à afficher. Il faudrait quand-même faire autrement.
Action 9 : Il faut traiter le parcours des collections, qui s’appelle
énumération des collections. Parfois, on peut surcharger l’obtention de
l’énumération et renvoyer une énumération sur un tableau de l’ensemble des
valeurs créé à cet instant afin d’éviter de lire une collection modifiée en
même temps et sans la bloquer, d’autant qu’on n’a pas d’info indiquant que
l’énumération se termine, quand elle se termine avant la fin.
En ce qui concerne les lignes, il faudrait
quand-même faire un blocage des modifications de la collection tant qu’un
objet d’énumération existe, retenus par un WeakReference, qui permet de
retenir l’objet sans comptage de référence, donc en le laissant partir quand
plus personne ne le retient, mais sa disparition n’est pas tout à fait
instantanée, et il n’existe pas d’événement lié à la disparition d’un objet,
il faudra utiliser un timer. Donc limiter ce traitement complexe à cette
collection, qui peut avoir beaucoup de lignes.
10.FAIRE UN OBJET DE COLLECTION DE BASE HERITANT DE
L’OBSERVABLECOLLECTION
ACTION 10 Afin de
sécuriser les collections au sein du projet, j’ai créé la mienne, avec des
locks qui entourent l’ensemble des méthodes.
La BlockingCollection ou la
ConcurrentDictionnary de Microsoft ne gère pas le parcours ni l’accès
indexé. Par contre, les collections concurrentes de Microsoft gèrent l’accès
réellement simultané, et non pas seulement synchronisé, et certaines d’entre
elles un accès plutôt rapide pour une gestion du multithread.
Il lui faut aussi quelque chose pour pouvoir
être bindée et envoyer des notifications de changement de collection. Mais à
l’usage, on remarquera que l’héritage de l’interface de Microsoft prévue à
cet effet ne suffit pas pour la DataGrid de WPF. Elle n’accepte de recevoir
que les événements réellement envoyés par l’objet ObservableCollection. J’ai
donc créé dans ma collection une propriété qui renvoie l’équivalent en
ObservableCollection. On remarque alors que lorsque cet événement est appelé
par une autre thread que la thread vidéo, WPF refuse l’événement avec une
erreur. On remarque aussi que cet événement est appelé dans la thread des
méthodes de modification de la collection. Donc, lors des méthodes de
modification de la collection initiale, je modifie aussi ce composant, à
moins qu’un accès sur la Thread Vidéo soit en cours, dans ce cas j’empile
dans une collection Queue les modifications à réaliser et les réalise dans
la thread vidéo en attendant qu’elle soit libre. Et si d’autres se
produisent pendant mon accès à moi, je les empile aussi. Je gère les locks
aussi dans l’énumération de ma collection initiale. Je gère une instruction
de chargement groupé des éléments, afin de ne réaliser qu’un lock unique,
afin de gagner du temps.
Avenir : Cette ObservableCollection pourrait
avoir un état. Les états seraient UnknownState, LoadingInProcess et Loaded.
Il faut une fonction pour la repasser à Unknown. Peut-être le Clear.
Avenir : Cette ObservableCollection peut avoir
une collection source observable et un délégué de drapeau d’ajout, afin de
se mettre à jour dès qu’une source change.
Avenir : L’écoute de l’événement
OnCollectionChanged ou de celui de la notification de premier parcours de
l’énumération peut provoquer des fuites de mémoire car un événement retient
à la fois l’écouté et l’écoutant. Utiliser aussi le projet qui assure des
événements dont l’écouté ne retient pas l’écoutant. Y réfléchir, mais pas
forcément faisable pour les outils de Microsoft, ils utilisent un vrai
événement, donc qui retient à la fois l’écoutant et l’écouté.
11.ASYNC AWAIT
Au fur et à mesure de l’évolution du Framework,
Microsoft est passé d’une gestion du multithread par simple classe thread et
des outils de verrouillage, par un moteur de threads qui gère le nombre de
threads actives, par une classe « Task » qui permet le contrôle et la
gestion de l’état de la thread, par une écriture des jonctions entre
threads, puis depuis 2012 par une syntaxe de la jonction des threads, qui
permet de les écrire comme du code normal alors qu’en fait, le compilateur
joint les blocs entre les points d’attente pour renvoyer au fin fond de son
compilateur un simple pointeur vers une tâche, qu’attendront les tâches dans
les parties de code qui suivent, et qui ne s’exécutent pas tout de suite, en
simulant l’exécution immédiate au développeur. Car une tâche emporte une
méthode à exécuter ultérieurement.
Depuis la version 2012 de Visual Studio, nous
avons un moyen pratique d’écrire les tâches à effectuer en multithread.
Il suffit désormais de préciser qu’une fonction
utilise une tâche asynchrone pour qu’elle soit asynchrone et qu’elle puisse
être appelée alors en asynchrone, mais dans un code qui reste parfaitement
linéaire.
Action 11 : Quelques fonctions bénéficieraient particulièrement d’une
écriture asynchrone dans ma bibliothèque. On peut faire très facilement
ainsi faire des tâches qui gèrent la concurrence d’une façon performante.
12.WAITASYNC
Mais les fonctions asynchrones ne permettent pas
d’utiliser à l’intérieur l’autre façon de gérer la concurrence que sont les
locks.
Un lock est interdit à l’intérieur de la méthode
directement écrite avec sa signature async, mais si un lock est exécuté dans
une méthode appelée au loin dans la hiérarchie par cette tâche async, cela
fait planter tout le logiciel de façon non systématique mais fréquente et
pas facilement débogable, d’autant que ce lock peut se trouver dans une DLL,
même du marché, et il peut s’en trouver fréquemment dans certaines DLL
tierces.
Il y a donc 2 méthodes qui ne doivent pas se
rencontrer.
Et si on va dans le détail, on se rend compte
que l’appel d’un lock au cours d’une fonction async await bloque facilement
l’application. Car même si on ne peut pas les écrire dans le même bloc, le
compilateur accepte d’appeler des méthodes qui en contiennent, il n’a pas
moyen de le savoir, il peut y avoir plusieurs niveaux, et des conditions, à
la fin toute la compilation se retrouverait globalement interdite.
Microsoft a créé une fonction,
SemaphoreSlim.WaitAsync, qui accepte d’être attendue par async. Deux
défauts : Elle est threadsafe, bien-sûr, mais :
1. Elle
bloque le thread en cours si celle-ci a déjà incrémenté le sémaphore une
première fois.
2. Car
WaitAsync ne sert à rien dans une procédure normale, à moins d’appeler son
Wait, mais alors, on bloquerait à nouveau l’application comme avec les
locks.
La solution est de passer soit par la fameuse
classe du framework Microsoft qui renvoie une tâche dont le résultat est
fourni de l’extérieur, soit de créer une tâche qui n’est pas démarrée.
Ensuite, quelle que soit la méthode utilisée, le processus extérieur peut
l’attendre, et la tâche en cours qui n’aura pas pu être rendue asynchrone
dans le même processus que son démarrage pourra exécuter la tâche-sémaphore
à sa sortie, ce qui provoquera la continuation des tâches qui l’attendaient.
Attention de bien gérer certains états intermédiaires qui peuvent survenir
de temps-en-temps sur la tâche-sémaphore.
13.PROBLEME AVEC WAIT
Une chose est sûre, bien des fois, on a à
utiliser des ressources ou des outils pour lesquels ont doit faire un péage
de locks, peut-être un nouveau design pattern, car on doit écrire une
barrière de locks pour la rendre threadsafe, comme la collection dont je
parlais. En tout cas une fonction qui ne permet qu’un thread à la fois pour
exécuter l’outil. Mais un tel outil est appelé par tellement de points
d’entrées que certains ne seront pas async, et on ne voudra pas s’embêter à
créer une version qui retourne une tâche pour chaque méthode de l’outil. Car
dès qu’une tâche appelle une méthode qui n’est pas une tâche, et que
celle-ci appelle de nouveau une méthode qui est une tâche, grâce à Wait,
cela bogue complètement.
De même, si on veut appeler une tâche de façon
empirique, afin de bloquer une méthode qui n’est pas une tâche le temps
qu’elle exécute une tâche, juste avec Task.Wait : Attention,
il y a un problème, cela ne peut pas être exécuté dans la thread vidéo,
car le Wait, contrairement au async, génère un lock dans la thread vidéo. Il
est dit qu’il s’agit du thread de synchronisation, j’ai constaté que même en
la forçant, elle reste la thread vidéo. Et si on comptait utiliser await au
lieu de Wait, il faudrait mettre async dans la signature de la méthode et
donc utiliser Wait dans la méthode appelante quand-même.
SSi une cellule (ligne+colonne) a besoin d’un
chargement différé, et que ce chargement nécessite des ressources, il faudra
réussir à soit éviter les Wait, qui bloquent la thread vidéo, soit que si
l’interface est à l’origine du chargement de la cellule, on retienne le
contrôle qui va stocker le résultat, mais qu’on fasse appel à un affichage
différé, qui permettra par exemple le async await, et l’affichage réel aura
lieu à partir d’une resynchronisation avec la thread vidéo.
14.BLOCS D’ETAPES
Note : Mais attention, si l’outil est récursif,
mais inter-objets, et inter-threads, il faudrait créer une méthode plutôt
qu’une attente, et cette méthode ne doit ni attendre ni libérer lors d’un
appel par une thread qui a déjà mis en attente. Il faudrait pratiquement
faire une collection des threads qui ont déjà appelé le sémaphore pour ne
plus le bloquer, ce serait très lent.
Bon, c’est utopique. Alors tournons-nous vers
une meilleure solution :o:p>
· Une
tâche-sémaphore à la fin de l’initialisation, dont on vérifie le thread
appelant dans toutes les fonctions qui doit être unique pour chaque phase
d’initialisation, qui sera exécuté dans la thread appelante,
· Une
tâche-sémaphore pour le chargement, dont on vérifie aussi de la même façon
que le thread est unique pendant la phase de chargement (mais pas de
déchargement), et le chargement pourrait être lancé dans un thread parallèle
(une instruction de déchargement sans gestion de thread mais tout de même
sécurisée pour pouvoir être appelée à tour de bras),
· Et
seules de petites opérations internes à la classe bloqueraient un troisième
thread qui verrouille les entrées pouvant modifier afin d’effectuer des
travaux en cours, étape précise identifiée telle quelle pour certains
travaux, par exemple un tri,
· Et
un blocage pour l’étape du Dispose, qui ne doit être appelé que lorsque
toutes les étapes en cours sont terminées ou prêtes.
· Toutes
les opérations de modification de l’objet mémorisant la vue, ou de sa
collection surtout, pouvant être appelées en parallèle, donc en phase
chargée, doivent absolument voir verrouiller tous leurs mécanismes pouvant
entrer en conflit par des locks, bien-sûr. Ce seront bien-sûr plutôt des
instructions de consultations, mais pas toujours.
Attention : En ce qui concerne l’étape de mise en production de la vue une
fois chargée, de nombreux threads risquent d’être à même de pouvoir
l’interroger en même temps.
Attention : Si la ressource a besoin d’un pipe de traitement parce qu’elle
n’est disponible que pour 1 traitement à la fois, il faut savoir débloquer
les autres accès aux données en mémoire. Surtout si le premier de ces
traitements est un chargement, long.
Attention : Les tâches d’accès aux données, j’entends par là à la classe de
collection une fois chargées, seront souvent concurrentielles, mais surtout,
dans le cas d’événements dynamiques de préparation, tels une surcharge, un
formatage, ou un calcul de la couleur, auront tellement besoin d’accéder à
l’ensemble du logiciel, aux fonctions courantes, qu’il n’y a pas moyen de
sécuriser les risques de conflits concurrentiels, même si la modification de
variables n’est pas courante pendant les opérations de consultation, tout
doit être fait pour verrouiller les risques d’altération. Par exemple un
drapeau qui empêcherait la récursivité pourrait empêcher l’entrée
concurrente dans la méthode…
Conclusion : C’est au développeur de vérifier et de verrouiller l’ensemble
des risques d’altération lorsqu’une opération entre au cœur du logiciel.
Tout le code potentiellement déroulable doit être vérifié en cas d’accès
concurrentiel.
· Parfois,
certains chargements à retardement se lancent lors de la première
consultation.
Il s’agit donc, dans ces cas précis, tel que le
fait la thread vidéo, de retenir le thread appelant à la première entrée, et
de la vérifier à l’entrée de chaque méthode concernée par la même étape et
qui ne peut être appelée que par le thread chargé de l’exécution de l’étape.
AAttention : Certaines de ces étapes vont
s’exécuter par tâche. Le bon moyen de vérifier la tâche en cours, qui peut
en exécuter d’autres, sera de faire en sorte que toutes les méthodes de
l’étape acceptent le même paramètre, un paramètre créé au lancement de
l’étape, avec un lock pour obtenir un jeton d’unicité du lancement de
l’étape, les autres lancements seront annulés ou alors on gèrera une
tâche-sémaphore d’annulation de la tâche d’étape en cours, et ce paramètre
sera systématiquement passé à toute fonction qui interviendra dans l’étape,
et appellera une fonction de la classe du paramètre qui vérifie si tout se
passe bien.
15.TÂCHE CONCURRENTE
Un grand besoin se fait alors sentir : Quand on
crée une tâche async, lorsque plusieurs sources en amont appellent cette
méthode, une tâche est créée et attendue pour chaque source.
En suffixant cette méthode par « Task », et en
suffixant une nouvelle méthode par « Concurrent », on peut stocker le
résultat de la méthode qui se termine par « Task » dans une variable de type
tâche. On ne fait cette opération que si la variable est vide. Ceci dans la
méthode qui se termine par « Concurrent ». Puis on attend la variable avec
« async », à la fin de la méthode qui se termine par « Concurrent ». C’est
cette méthode « Concurrent » qu’on appellera dans les différentes sources
d’appel en amont. Ceci leur permettra d’attendre la même tâche, de continuer
toutes leur exécution dès que la tâche sera terminée, et de ne pas
l’attendre si elle est déjà terminée.o:p>
LLorsque la tâche unique à attendre doit être
attendue de nouveau, il suffit de repasser la variable à null, dans le bon
ordre s’il y en a plusieurs, dans une tâche concurrente elle aussi afin
qu’on ne puisse pas la relancer pendant sa propre exécution.
16.LE MINIMUM DE REQUÊTES SYNCHRONES
Certains outils d’accès aux données ne
permettent que de faire une requête à la fois. En mettant dans les classes
d’accès aux données un verrou qui empêche d’exécuter une requête synchrone
pendant le chargement de la requête asynchrone, ainsi qu’une requête
asynchrone dans une thread différente d’une requête synchrone en train de
finir son remplissage et donc ayant libéré le verrou habituel, on rend
toutes les requêtes synchrones dès qu’il y a une requête synchrone. Par
conséquent, on déduit que l’exécution simultané de requêtes synchrones et
asynchrones peuvent bloquer des threads.
ACTION 12 Dans
ce sens, il s’agirait de passer les procédures qui font des requêtes soit en
les arrêtant à la requête et en faisant une fonction de reprise, attention
aux deadlocks si cette attente se fait plus d’une fois, soit en utilisant
async/await.
17.CONCLUSION
Il y a le code linéaire, mais le passage du code
linéaire en code potentiellement concurrent par l’intermédiaire de tâches ou
de threads doit être entièrement relu. Dans un fonctionnement multithread,
aucune possibilité d’altération concurrentielle ne doit être laissée à la
traîne, oubliée. Chaque cas oublié sera un jour l’objet d’une collision. On
ne peut corriger un code multithread en corrigeant chaque cas sur lequel on
tombe. Il faut savoir si on a affaire à quelques cas résiduels ou des
milliers de risques potentiels à tout moment. Il ne faut jamais tomber dans
ce deuxième cas. On ne pourra jamais obtenir un logiciel du tout stable avec
la correction à la volée. Il faut d’abord avoir moyen d’avoir un code dont
on sait que les cas collisionnels sont tous identifiés, tous notoirement
corrigés avant les tests.
Le multithread n’est pas complexe à comprendre,
mais difficile à écrire et s’enchaîne de façon complexe à l’exécution. C’est
pour cette raison que même les logiciels des grandes marques ont des sautes
d’humeur incompréhensibles, aléatoires, tel Siri ou les simples rappels de
Apple, ou comme ici sous Word la touche Shift est restée logiciellement
coincée. A noter que c’est aussi dû au manque de transactions dans les bases
big data. Ces imperfections sont rattrapées par des rechargements
automatiques, intempestifs, des
autocorrections supplémentaires, des imperfections auxquelles on est habitué
avec Internet, mais même les autocorrections doivent gérer les accès
concurrentiels. L’autocorrection n’est pas une méthode, et n’est en rien
valide, fiable ou récursif.o:p>
C’est au développeur de vérifier et de
verrouiller l’ensemble des risques d’altération lorsqu’une opération entre
au cœur du logiciel. Tout le code potentiellement déroulable doit être
vérifié en cas d’accès concurrentiel. Il n’y a pas de méthode pour toutes
les parties du logiciel dans le domaine du développement multithread. D’où
l’idée aussi de limiter les interactions, la taille du code face à l’accès
concurrentiel, encapsuler, profiter de l’encapsulation du langage pour
élargir celle-ci, et il ne faut pas hésiter à se donner et communiquer des
règles, particulièrement d’accès, une encapsulation supplémentaire régulée,
et des méthodes de modification, des règles d’enveloppes qui sécurisent
celles-ci.