POURQUOI CE BLOG, POUR QUI ?


POURQUOI CE BLOG, POUR QUI ?
Ce Blog s'adresse à tous ceux qui sont passionnés par les sciences informatiques , Professionnels,Etudiants,Amateurs ...
Les sujets exposés dans la suite se rapporteront essentiellement sur l'analyse informatique,la programmation,le développement ainsi que à l'architecture IT.
QUI SUIS JE ?
Je suis Kangulungu Lubilanji, Consultant-Freelance sur les technologies .NET,C#,ASP.NET ... Contactez moi pour plus d'informations.

La Programmation OO: Chapitre 17. Multithreading

Chapitre 17. Multithreading
Ce chapitre est consacré au multithreading, qui permet à plusieurs objets d'agir de manière simultanée, tout en synchronisant leur accès à des ressources qu'il ne leur est pas possible de partager

Multithreading, le mécanisme dit de « multithreading », est qu'il est possible de directement exploiter dans l'ecriture de code C#, rend effectif ce parallélisme des taches évoqué dans la précédente section. Le multithreading permet aux blocs d'instructions de s'imbriquer pendant leur exécution.
Le processeur passera la main à un bloc puis à l'autre de manière séquentielle. Les instructions continueront à s'exécuter en séquence (le processeur ne pouvant toujours exécuter qu'une seule instruction à la fois) alors que les blocs, eux, s'exécuteront en parallèle. Chacun de ces blocs, sujet à ce parallélisme d'exécution, sera appelé « thread ». Comme la figure ci-après le montre, le processeur passera d'un thread à l'autre, sous la responsabilité d'un « gestionnaire de thread », qui saura quand interrompre un thread pour en débuter un autre. Cette gestion du multithreading est généralement laissée au système d'exploitation. Dans la version la plus simple, le processeur se consacre à chacun des threads pendant une même durée. Des stratégies plus fines permettent, par exemple, d'interrompre un thread, quand celui-ci est en attente d'une ressource ou d'un accès périphérique, pour reprendre l'exécution d'un autre.
Malgré la volonté de C# (très dépendant du ou des Windows) de s'émanciper autant que faire se peut des plates-formes sur lesquelles ils s'exécutent (pour que les applications tournent de la même manière, quelle que soit cette plate-forme), le résultat d'un programme intégrant un mécanisme de multithreading pourra largement varier d'une plate-forme à l'autre. Ce mécanisme est, à une moindre échelle (car à l'intérieur d'un seul et même programme), la réplique exacte du mécanisme du multitâche, présents dans tous les systèmes d'exploitation actuels.

Le multithreading, une réplique à moindre échelle du multitâche, le multitâche vous permet d'utiliser différentes applications, Word, Firefox, votre jeu favori ... avec la même apparence de simultanéité. Le multitâche et sa version réduite, le multithreading, s'exécutent de la même façon. Cela se déroule de la manière suivante : le gestionnaire entame un première mini-tâche (thread), en exécute quelques instructions, puis est interrompu pour donner la main à une deuxième mini-tâche. Avant de passer la main, il mémorise tout ce qu'il est nécessaire pour pouvoir reprendre l'exécution de la première mini-tâche, à peine délaissée, par exemple, l'adresse de l'instruction suivante à exécuter ou les adresses de fichiers et autres périphériques avec lesquels cette mini-tâche interagissant. En substance, il mémorise tout le contexte d'exécution ce mini-tâche. Après avoir donné un peu de son temps à toutes les mini-tâches qui s'exécutent à la queue leu leu et pourtant parallèlement, il revient à la première, en commençant, avant toute chose, par restituer son contexte d'exécution. Tout comme pour le multitâche, c'est le système d'exploitation qui prend en charge la gestion du temps à répartir entre tous les threads.
On dit des threads qu'ils sont composés de trois compartiments, comme le montre la figure ci-après. Les trois compartiments sont : le processeur sur lequel le thread s'exécute, les données qu'il manipule (par exemple, les attributs de l'objet, l'instance de la classe dans laquelle le thread est défini), et le corps d'instructions, la mini-tâche, associé au thread.

 Cette mini-tâche est transmise en C#, en recourant à une nouvelle structure propre à ce langage (et .Net en général), et que l'on appelle les « délégués ». Un délégué - dont la création et le mode d'utilisation ressemble à s'y méprendre aux interfaces (mais qui ne contiendrait qu'une et une méthode à concrétiser) - produit des instances associées, qui se limitent à n'être que des référents de fonction. Un délégué pointe sur une méthode, au même titre qu'un référent pointe sur un objet. Le délégué utilisé est de type ThreadStart, et sa seule raison d'être est de pointer sur le bloc d'instructions à associer, en C#, à un thread.
C#4 a considérablement innové en matière de multithreading, de manière à ne pas gaspiller inutilement les objets thread. Ainsi, si un objet thread existe déjà mais n'est plus utilisé, il pourra être facilement réexploité.
Ces facilités devraient conduire à améliorer les performances des exécutables répartis sur plusieurs threads qui ont tendance à s'écrouler rapidement avec la multiplication de ceux-ci.

class Eau {
        private int quantite;
        public Eau(int quantite ) 
        {
            this.quantite = quantite;
        }
        public void diminue(int decroit) {
            if (quantite > decroit)
            {
                Console.WriteLine("ok, l'eau diminue");
                quantite -= decroit;
            }
            else
            {
                quantite = 0;
                Console.WriteLine("Zut, il n'y plus d'eau");
            }
        }
    }
    class Predateur {
        private Eau uneEau;
        private Thread unThread;

        public Predateur(Eau uneEau)
        {
            this.uneEau = uneEau;
            ThreadStart unTs = new ThreadStart(jeBois);
            unThread = new Thread(unTs);
        }
        public void lanceThread() 
        {
            unThread.Start();        
        }
        public void jeBois()
        {
            for(int i=0; i<100;i++)
            {
                Console.WriteLine("le predateur essaie de boire");
                uneEau.diminue(20);
            }
        }

    }
    class Proie {
        private Eau uneEau;
        private Thread unThread;

        public Proie(Eau uneEau) {
            this.uneEau = uneEau;
            unThread = new Thread(new ThreadStart(jeBois));
        }
        public void lanceThread()
        {
            unThread.Start();
        }
        public void jeBois()
        {
            for (int i = 0; i < 100; i++)
            {
                Console.WriteLine("la proie essaie de boire");
                uneEau.diminue(10);
            }
        }
    }
    class Jungle
    {
        static void Main(string[] args)
        {
            Eau uneEau = new Eau(1000);
            Proie uneProie = new Proie(uneEau);
            Predateur unPredateur = new Predateur(uneEau);
            uneProie.lanceThread();
            unPredateur.lanceThread();
            Console.ReadLine();
        }
    }
L'impact du multithreading sur les diagrammes de séquence UML, il est utile de refaire un petit détour par l'UML et ses diagrammes de séquence, de manière à différencier les diagrammes correspondant aux deux pratiques, sans et avec mutithreading.
Diagramme de séquence de la proie et du prédateur se désaltérant sans multithreading

Diagramme de séquence de la proie et du prédateur se désaltérant avec multithreading

lorsqu'un message est déclenché de manière synchrone, la flèche qui le représente dans le diagramme de  séquence est complète, et les « rectangles de temporalité » s'ajustent en fonction. Cela veut dire qu'en l'absence de multithreading, le premier message jeBois, envoyé à la proie, doit se terminer avant que le second message jeBois, envoyé cette fois au prédateur, ne puisse débuter. En revanche, lorsqu'un message est déclenché de manière asynchrone la flèche qui le représente est différemment dessinée, et les « rectangles de temporalité » n'ont plus à s'ajuster en fonction.
En fait, dans ces cas, le déroulement de l'expéditeur du message n'est plus conditionné par le déroulement du destinataire. Les deux objets vivent leur propre vie, indépendamment l'un de l'autre. L'eau exécute deux fois le même message, de manière parallèle, comme le diagramme de séquence l'indique également. On conçoit que ce même type de diagramme de séquence puisse se trouver lors de la réalisation d'applications distribuées, dans la mesure où l'objet expéditeur et l'objet destinataire sont  actifs sur des processeurs différents.

Du multithreading aux applications distribuées, du multithreading aux applications distribuées, il n'y a qu'un pas, car deux objets, exécutant parallèlement deux blocs d'instructions séparés, pourraient idéalement se trouver sur deux processeurs différents. Lorsqu'un message est envoyé à travers Internet, il n'y a a priori aucune raison pour que son expéditeur se tourne les pouces, en attendant qu'il finisse de s'exécuter. De fait, tous les protocoles d'objets distribués autorisent, d'une manière ou d'une autre, des envois des messages asynchrones, qui n'interrompent pas le déroulement de l'exécution de l'expéditeur.

Choisir entre un envoi synchrone ou asynchrone ne dépend in fine en rien de l'architecture informatique qui supporte cet envoi de message, mais bien de la seule logique de l'application à exécuter. L'objet expéditeur a-t'il, oui ou non, besoin d'un réponse du destinataire pour aller de l'avant avec son flot d'instructions ? Si la réponse est non, il pourra continuer à s'exécuter, soit sur un thread qui lui est propre, dans le cas d'une informatique purement séquentielle, soit sur un processeur qui lui est propre, dans le cas d'une informatique qui peut être parallélisée au moins sur deux processeur.

Dans la pratique, les raisons d'exploiter le multithreading sont nombreuses. Les applications de types e-commerce, où plusieurs client veulent bénéficier simultanément des services d'un serveur, ne peuvent se réaliser autrement qu'en  multithreading : un client, un thead. Il sera important, dans ce cas, de synchroniser les multiples interactions client-serveur afin d'établir, avec prudence, à quel moment de l'interaction on peut passer d'un client à l'autre.

Les animations graphiques, si fréquentes dans les pages web, exigent généralement un thread qui leur est propre, afin que leur déroulement continu (souvent, une boucle infinie opérant sur un vecteur d'images) n'empêche pas de regagner le contrôle du processeur pour d'autres interactions. 

Enfin, les architectures des ordinateurs étant de plus en plus souvent multiprocesseur, il est clair que la meilleure manière de garantir l'utilisation en parallèle de leurs processeurs à l'exécution des programmes est d'exploiter la possibilité du multitherading.

Des threads équirépartis, nous aimerions reprendre un certain contrôle sur la façon dont la proie et le prédateur consomment l'eau. Idéalement, la proie puis le prédateur, et ce de façon alternée, devraient pouvoir se désaltérer. Il y a plusieurs façons d'y arriver. Une première, très simple, est d'utiliser la méthode yield() dans la boucle de la méthode jeBois(). Cette méthode interrompt le thread, juste le temps pour le gestionnaire de pouvoir donner la main à un autre thread. Comme il n'y en a que deux ici c'est automatiquement le second qui prendra le contrôle du processeur et, ainsi de suite, de façon parfaitement équitable.

Une autre manière est de jouer sur la priorité des threads. On peut attribuer aux threads différents niveaux de  priorité, qui permettrons à certains de mobiliser plus souvent  le processeur que d'autres. Un dernière manière est de recourir à la méthode sleep(). La méthode sleep() force le thread à se mettre en veille pendant la durée, exprimée en milliseconde,transmise comme argument de la méthode.
Cette méthode permet d'ajuster la vitesse d'exécution du thread, car plus la durée de mise veille est importante, plus le temps mis par l'exécution de thread sera rallongé. La méthode sleep() est déclarée statique dans la classe Thread(en C# elle ne peut s'appeler qu'à partir de sa classe car C#, ne permet pas à une méthode statique d'être appelée à partir d'un objet).

Synchroniser les threads, dans les applications d'entreprises, des bases de données, quand elles sont accédées par plusieurs utilisateurs à la fois. Si les modifications apportées par le premier utilisateur peuvent avoir un impact sur celles que cherche à réaliser un second, il est primordial de laisser le premier aller jusqu'au bout de sa manoeurvre.

En l'absence de tel souci de synchronisation, les résultats deviendront parfaitement incohérents. Supposez par exemple des réservations de billet d'avions dans une même base de données. Il est indispensable qu'une première réservation se termine entièrement avant de lancer une deuxième (à partir d'un lien physique différent et indépendant du premier). Si ce n'était pas le cas et si le guichetier confirme sa réservation pourrait être effectuée ailleurs par un autre guichetier. Dans un cas semblable, c'est en effet la base de données qui doit s'occuper de la synchronisation de son accès par les différents programmes qui cherchent à la modifier. Les longues attentes que vous devez parfois subir dans les aéroports pour la réservation information d'un place d'avion sont la conséquence de ce souci de synchronisation. Attentes qui valent mieux que de retrouver sur vos genoux dans l'avion le passager qui vous suivait dans la file.

Il est important de comprendre que l'arbitre du mécanisme synchronisation entre threads est la ressource elle-même, cette même ressource qu'il est impossible de partager. Cette synchronisation  s'opère car le thread confie à la ressource un « sémaphore » qui le temps d'une partie de son exécution, n'accepte pas que soit partagée cette ressource. Si un thread un autre thread veut utiliser cette même ressource, il devra attendre que celle-ci ait libéré le sémaphore qu'elle a en sa possession. Cette libération effectuée, un autre thread pourra déclencher la méthode utilisant cette ressource, quitte à ce qu'il redonne, si lui aussi cherche à synchroniser cet accès, le sémaphore à peine récupéré à la ressource.
En C# cette synchronisation est obtenue soit par le mot-clé lock, soit en encadrant la partie de code à synchroniser par Monitor.Enter() et Monitor.Exit()
Sémaphore, la détention par la ressource à synchroniser d'un « sémaphore », le temps de son utilisation, de telle manière que d'autres thread n'y aient plus accès, est la base de cette pratique de synchronisation. Cela permet à la ressource, elle-même, d'arbitrer son utilisation.


Aucun commentaire: