Intégration globale du multithread C#.Net, version 4 du 23 juin 2018

sans titre 1

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.

Print | posted on Friday, July 6, 2018 12:39 AM

Copyright © Frédéric Decréquy

Design by Bartosz Brzezinski

Design by Phil Haack Based On A Design By Bartosz Brzezinski