Applications multi-thread. Architectures d'applications multithreads Applications multithreads

Les threads et les processus sont des concepts liés en informatique. Les deux sont des séquences d'instructions qui doivent être exécutées dans un ordre spécifique. Cependant, les instructions dans des threads ou des processus séparés peuvent s'exécuter en parallèle.

Des processus existent dans le système d'exploitation et correspondent à ce que les utilisateurs considèrent comme des programmes ou des applications. Un fil, en revanche, existe au sein d'un processus. Pour cette raison, les threads sont parfois appelés « processus légers ». Chaque processus se compose d'un ou plusieurs threads. L'existence de plusieurs processus permet à un ordinateur d'effectuer « simultanément » plusieurs tâches. L'existence de plusieurs threads permet à un processus de partager le travail pour une exécution parallèle. Sur un ordinateur multiprocesseur, les processus ou les threads peuvent s'exécuter sur différents processeurs. Cela permet un vrai travail en parallèle.

Un traitement entièrement parallèle n'est pas toujours possible. Les flux ont parfois besoin de se synchroniser. Un thread peut attendre le résultat d'un autre thread, ou un thread peut avoir besoin d'un accès exclusif à une ressource qui est utilisée par un autre thread. Les problèmes de synchronisation sont une cause fréquente d'erreurs dans les applications multithread. Parfois, un thread peut finir par attendre une ressource qui ne sera jamais disponible. Cela aboutit à une condition appelée blocage.

La première chose à apprendre est le processus consiste en au moins un fil... Dans le système d'exploitation, chaque processus a un espace d'adressage et un seul thread de contrôle. En fait, cela détermine le processus.

Un côté, un processus peut être considéré comme un moyen de combiner des ressources connexes en un seul groupe... Un processus a un espace d'adressage qui contient le texte et les données du programme, ainsi que d'autres ressources. Les ressources sont des fichiers ouverts, des processus enfants, des alarmes non gérées, des gestionnaires de signaux, des informations d'identification, etc. Il est beaucoup plus facile de gérer les ressources en les regroupant sous la forme d'un processus.

D'un autre côté, un processus peut être considéré comme un fil de co-commandes exécutables ou simplement comme un fil... Le thread a un compteur de commandes qui garde une trace de l'ordre dans lequel les actions sont effectuées. Il a des registres qui stockent les variables courantes. Il a une pile contenant un protocole d'exécution de processus, où une trame distincte est allouée pour chaque procédure appelée mais pas encore retournée. Bien qu'un thread doive s'exécuter au sein d'un processus, vous devez faire la distinction entre les concepts de thread et de processus. Les processus sont utilisés pour regrouper les ressources et les threads sont des objets qui s'exécutent alternativement sur le CPU.

Le concept de flux ajoute au modèle de processus possibilité d'exécution simultanée de plusieurs programmes dans le même environnement de processus sont assez indépendants. Plusieurs threads s'exécutant en parallèle dans un seul processus, c'est comme avoir plusieurs processus s'exécutant en parallèle sur le même ordinateur. Dans le premier cas, les threads partagent l'espace d'adressage, les fichiers ouverts et d'autres ressources. Dans le second cas, les processus partagent la mémoire physique, les disques, les imprimantes et d'autres ressources. Les flux ont certaines des propriétés des processus, c'est pourquoi ils sont parfois appelés processus légers. Terme multithreadingégalement utilisé pour décrire l'utilisation de plusieurs threads dans un seul processus.

Tout flux se compose de deux composants :

objet noyauà travers lequel le système d'exploitation contrôle le flux. Des informations statistiques sur le flux y sont également stockées (des flux supplémentaires sont également créés par le noyau) ;
pile de fils, qui contient les paramètres de toutes les fonctions et variables locales dont le thread a besoin pour exécuter le code.

Résumant la ligne, corrigeons : la principale différence entre les processus et les threads, consiste dans le fait que les processus sont isolés les uns des autres, ils utilisent donc des espaces d'adressage différents, et les threads peuvent utiliser le même espace (à l'intérieur du processus) tout en effectuant des actions sans interférer les uns avec les autres. C'est quoi commodité de la programmation multithread: En divisant l'application en plusieurs threads séquentiels, nous pouvons augmenter les performances, simplifier l'interface utilisateur et atteindre l'évolutivité (si votre application est installée sur un système multiprocesseur, exécutant des threads sur différents processeurs, votre programme s'exécutera à une vitesse étonnante =)) .

1. Un thread définit la séquence d'exécution du code dans un processus.

2. Le processus n'exécute rien, il sert simplement de conteneur pour les threads.

3. Les cours d'eau sont toujours créés dans le contexte d'un processus, et toute leur vie ne se déroule qu'à l'intérieur de ses limites.

4. Les threads peuvent exécuter le même code et manipuler les mêmes données, ainsi que partager des descripteurs d'objets du noyau, car la table de descripteurs n'est pas créée dans des threads séparés, mais dans des processus.

5. Étant donné que les threads consomment beaucoup moins de ressources que les processus, essayez de résoudre vos problèmes en utilisant des threads supplémentaires et évitez de créer de nouveaux processus (mais soyez intelligent à ce sujet).

Multitâche(eng. multitâche) - une propriété du système d'exploitation ou de l'environnement de programmation pour permettre le traitement en parallèle (ou pseudo-parallèle) de plusieurs processus. Le véritable multitâche du système d'exploitation n'est possible que dans les systèmes informatiques distribués.

Fichier : capture d'écran de Debian (version 7.1, "Wheezy") exécutant l'environnement de bureau GNOME, Firefox, Tor et VLC Player.jpg

Le bureau d'un système d'exploitation moderne, reflétant l'activité de plusieurs processus.

Il existe 2 types de multitâche :

· Processus multitâche(basé sur des processus - programmes exécutés simultanément). Ici, un programme est le plus petit morceau de code qui peut être contrôlé par le planificateur du système d'exploitation. Mieux connu de la plupart des utilisateurs (travaillant dans un éditeur de texte et écoutant de la musique).

· Thread multitâche(basé sur le flux). La plus petite unité de code managé est un thread (un programme peut exécuter 2 tâches ou plus en même temps).

Le multithreading est une forme spécialisée de multitâche.

1 Propriétés d'un environnement multitâche

2 Difficultés à mettre en place un environnement multitâche

3 Histoire des systèmes d'exploitation multitâches

4 types de multitâche pseudo-parallèle

o 4.1 Multitâche non préemptif

o 4.2 Multitâche collaboratif ou coopératif

o 4.3 Multitâche préemptif ou prioritaire (mode temps réel)

5 Situations problématiques dans les systèmes multitâches

o 5.1 Famine

o 5.2 Condition de course

· 7 notes

Propriétés d'un environnement multitâche [modifier | modifier la source]

Les environnements multitâches primitifs fournissent un "partage des ressources" propre où chaque tâche se voit attribuer un morceau de mémoire spécifique et la tâche est appelée à des intervalles spécifiques.

Les systèmes multitâches plus avancés allouent les ressources de manière dynamique lorsqu'une tâche démarre en mémoire ou quitte la mémoire, en fonction de sa priorité et de la stratégie du système. Cet environnement multitâche présente les caractéristiques suivantes :

Chaque tâche a sa propre priorité, en fonction de laquelle elle reçoit du temps processeur et de la mémoire

Le système organise des files d'attente de tâches afin que toutes les tâches reçoivent des ressources, en fonction des priorités et de la stratégie du système

Le système organise le traitement des interruptions, selon quelles tâches peuvent être activées, désactivées et supprimées

· À la fin de la tranche de temps spécifiée, le noyau transfère temporairement la tâche de l'état d'exécution à l'état prêt, donnant des ressources à d'autres tâches. S'il n'y a pas assez de mémoire, les pages de tâches non-exécutables peuvent être préemptées sur disque (swapping), puis, après un certain temps par le système, restaurées en mémoire

Le système protège l'espace d'adressage de la tâche contre les interférences non autorisées provenant d'autres tâches

Le système protège l'espace d'adressage de son noyau contre les interférences non autorisées des tâches

Le système détecte les plantages et les blocages de tâches individuelles et les arrête

Le système résout les conflits d'accès aux ressources et aux appareils, évitant les blocages de blocage général dû à l'attente de ressources verrouillées

Le système garantit à chaque tâche qu'elle sera tôt ou tard activée

Le système traite les demandes en temps réel

Le système assure la communication entre les processus

Difficultés à mettre en œuvre un environnement multitâche [modifier | modifier la source]

La principale difficulté de la mise en œuvre d'un environnement multitâche est sa fiabilité, qui se traduit par la protection de la mémoire, la gestion des pannes et des interruptions, la prévention des blocages et des interblocages.

En plus d'être fiable, un environnement multitâche doit être efficace. Le coût des ressources pour sa maintenance ne doit pas: interférer avec les processus, ralentir leur travail, limiter fortement la mémoire.

Multithreading- une propriété d'une plate-forme (par exemple, un système d'exploitation, une machine virtuelle, etc.) ou une application qu'un processus engendré dans un système d'exploitation peut consister en plusieurs ruisseaux fonctionnant « en parallèle », c'est-à-dire sans ordre prescrit dans le temps. Pour certaines tâches, cette séparation peut permettre une utilisation plus efficace des ressources informatiques.

Tel ruisseaux aussi appelé fils d'exécution(de l'anglais. fil d'exécution); parfois appelé « threads » (traduction littérale de l'anglais. fil) ou de manière informelle « threads ».

L'essence du multithreading est le quasi-multitâche au niveau d'un processus exécutable, c'est-à-dire que tous les threads sont exécutés dans l'espace d'adressage du processus. De plus, tous les threads d'un processus ont non seulement un espace d'adressage commun, mais également des descripteurs de fichiers communs. Un processus en cours d'exécution a au moins un thread (principal).

Le multithreading (en tant que doctrine de programmation) ne doit pas être confondu avec le multitâche ou le multitraitement, malgré le fait que les systèmes d'exploitation qui implémentent le multitâche implémentent généralement également le multithreading.

Les avantages du multithread en programmation sont les suivants :

· Simplification du programme dans certains cas par l'utilisation d'un espace d'adressage commun.

· Moins de temps consacré à la création d'un flux par rapport au processus.

· Augmentation des performances des processus grâce à la parallélisation des calculs du processeur et des opérations d'E/S.

1 Types de mise en œuvre de flux

2 Interaction des fils

3 Critique de la terminologie

· 6 notes

Types de mise en œuvre de flux [modifier | modifier la source]

· Stream dans l'espace utilisateur. Chaque processus a une table de threads similaire à la table de processus du noyau.

Les avantages et inconvénients de ce type sont les suivants : Inconvénients

1. Absence d'interruption de minuterie dans un processus

2. Lors de l'utilisation d'une requête système bloquante pour un processus, tous ses threads sont bloqués.

3. Complexité de la mise en œuvre

· Thread dans l'espace noyau. En plus de la table de processus, il existe une table de threads dans l'espace noyau.

· "Fibres" (eng. fibres). Plusieurs threads en mode utilisateur s'exécutant dans un seul thread en mode noyau. Le thread de l'espace noyau consomme des ressources notables, principalement de la mémoire physique et la plage d'adresses en mode noyau pour la pile en mode noyau. Par conséquent, le concept de "fibre" a été introduit - un flux léger exécuté exclusivement en mode utilisateur. Chaque flux peut avoir plusieurs « fibres ».

Interaction des fils [modifier | modifier la source]

Dans un environnement multithread, des problèmes surviennent souvent liés à l'utilisation des mêmes données ou périphériques par des threads s'exécutant en parallèle. Pour résoudre de tels problèmes, des méthodes d'interaction de threads telles que des exclusions mutuelles (mutex), des sémaphores, des sections critiques et des événements sont utilisées.

· Les exclusions mutuelles (mutex, mutex) sont un objet de synchronisation qui est défini sur un état de signal spécial lorsqu'il n'est occupé par aucun thread. Un seul thread possède cet objet à un moment donné, d'où le nom de tels objets (de l'anglais muet habituellement ex accès exclusif - accès mutuellement exclusif) - l'accès simultané à une ressource partagée est exclu. Une fois toutes les actions nécessaires prises, le mutex est libéré, donnant aux autres threads l'accès à la ressource partagée. L'objet peut prendre en charge la capture récursive une seconde fois par le même thread, incrémentant le compteur sans bloquer le thread, puis nécessitant plusieurs désallocations. C'est, par exemple, la section critique de Win32. Cependant, certaines implémentations ne prennent pas en charge cela et provoquent un blocage du thread lors d'une tentative de capture récursive. C'est FAST_MUTEX dans le noyau Windows.

· Les sémaphores représentent les ressources disponibles qui peuvent être acquises par plusieurs threads en même temps jusqu'à ce que le pool de ressources soit vide. Ensuite, les threads supplémentaires doivent attendre jusqu'à ce que la quantité de ressources requise soit à nouveau disponible. Les sémaphores sont très efficaces car ils permettent un accès concurrent aux ressources. Un sémaphore est une extension logique d'un mutex - un sémaphore avec un compteur de 1 est équivalent à un mutex, mais le compteur peut être supérieur à 1.

· Développements. Un objet qui stocke 1 bit d'information « signalé ou non », sur lequel les opérations « signal », « remise à l'état non-signalé » et « attente » sont définies. L'attente d'un événement signalé est l'absence d'opération avec poursuite immédiate de l'exécution du thread. L'attente d'un événement non sélectionné entraîne la suspension du thread jusqu'à ce qu'un autre thread (ou la deuxième phase du gestionnaire d'interruption dans le noyau du système d'exploitation) signale l'événement. Il est possible d'attendre plusieurs événements dans les modes "tout" ou "tout". Il est également possible de créer un événement qui est automatiquement réinitialisé à un état non signalé après le réveil du premier et unique thread en attente (un tel objet sert de base à la mise en œuvre de l'objet "section critique"). Ils sont activement utilisés dans MS Windows, à la fois en mode utilisateur et en mode noyau. Il existe un objet similaire dans le noyau Linux appelé kwait_queue.

· Les sections critiques fournissent une synchronisation similaire aux mutex, sauf que les objets représentant les sections critiques sont disponibles dans le même processus. Les événements, les mutex et les sémaphores peuvent également être utilisés dans une application à processus unique, mais les implémentations de section critique dans certains systèmes d'exploitation (tels que Windows NT) fournissent un mécanisme de synchronisation mutuellement exclusif plus rapide et plus efficace - les opérations d'obtention et de libération sur le section sont optimisés pour le cas d'un seul thread (pas de contention) afin d'éviter tout appel système menant au noyau de l'OS. Comme les mutex, un objet représentant une section critique ne peut être utilisé que par un thread à la fois, ce qui les rend extrêmement utiles pour délimiter l'accès aux ressources partagées.

· Variables conditionnelles (condvars). Ils sont similaires à des événements, mais ce ne sont pas des objets qui occupent de la mémoire - seule l'adresse d'une variable est utilisée, le concept de "contenu d'une variable" n'existe pas, l'adresse d'un objet arbitraire peut être utilisée comme variable conditionnelle . Contrairement aux événements, définir une variable de condition sur un état signalé n'a aucune conséquence s'il n'y a actuellement aucun thread en attente sur la variable. Définir un événement dans un cas similaire implique de stocker l'état « signalé » à l'intérieur de l'événement lui-même, après quoi les prochains threads souhaitant attendre l'événement continuent l'exécution immédiatement sans s'arrêter. Pour tirer pleinement parti d'un tel objet, il est également nécessaire de libérer le mutex et d'attendre la variable conditionnelle de manière atomique. Ils sont largement utilisés dans les systèmes d'exploitation de type UNIX. Les discussions sur les avantages et les inconvénients des événements et des variables de condition sont une partie notable des discussions sur les avantages et les inconvénients de Windows et UNIX.

Port d'achèvement d'E/S (IOCP). L'objet "file d'attente" implémenté dans le noyau du système d'exploitation et accessible via des appels système avec les opérations "mettre la structure en queue de file d'attente" et "prendre la structure suivante en tête de file d'attente" - le dernier appel interrompt l'exécution de le thread si la file d'attente est vide et jusqu'à ce qu'un autre thread ne fasse pas l'appel à "put". La caractéristique la plus importante d'IOCP est que des structures peuvent y être placées non seulement par un appel système explicite depuis le mode utilisateur, mais aussi implicitement dans le noyau du système d'exploitation à la suite d'une opération d'E/S asynchrone effectuée sur l'un des descripteurs de fichier. Pour obtenir cet effet, vous devez utiliser l'appel système "lier le descripteur de fichier à IOCP". Dans ce cas, la structure placée dans la file d'attente contient le code d'erreur de l'opération d'E/S, et aussi, si cette opération est réussie, le nombre d'octets réellement entrés ou sortis. L'implémentation du port d'achèvement limite également le nombre de threads qui s'exécutent sur un seul processeur/cœur après la mise en file d'attente d'une structure. L'objet est spécifique à MS Windows et permet de traiter les demandes de connexion entrantes et les blocs de données dans le logiciel serveur dans une architecture où le nombre de threads peut être inférieur au nombre de clients (il n'est pas nécessaire de créer un thread séparé avec une consommation de ressources pour chaque nouveau client).

· ERESOURCE. Un mutex qui prend en charge la capture récursive, avec une sémantique de capture partagée ou exclusive. Sémantique : un objet peut être soit libre, soit capturé par un nombre arbitraire de threads de manière partagée, ou capturé par un seul thread de manière exclusive. Toute tentative de capture qui enfreint cette règle bloquera le thread jusqu'à ce que l'objet soit libéré afin que la capture soit autorisée. Il existe également des opérations telles que TryToAcquire - il ne bloque jamais un thread, qu'il capture ou (si un verrou est nécessaire) qu'il renvoie FALSE sans rien faire. Il est utilisé dans le noyau Windows, en particulier dans les systèmes de fichiers - par exemple, tout fichier disque ouvert correspond à une structure FCB, dans laquelle il existe 2 de ces objets pour synchroniser l'accès à la taille du fichier. L'une d'entre elles - la ressource E/S de pagination - est capturée exclusivement dans le chemin de troncature du fichier et garantit qu'au moment de la troncature, il n'y a pas d'E/S active à partir du cache et du mappage mémoire sur le fichier.

Protection contre les pannes. Objet semi-documenté (les appels sont présents dans les fichiers d'en-tête, mais pas dans la documentation) dans le noyau Windows. Compteur avec opérations "augmenter", "diminuer" et "attendre". Attendre bloquera le thread jusqu'à ce que les opérations de décrémentation décrémentent le compteur à zéro. En outre, l'opération d'incrémentation peut échouer et la présence d'un temps d'attente actuellement actif entraîne l'échec de toutes les opérations d'incrémentation.

Les articles précédents parlaient du multithreading sous Windows à l'aide de CreateThread et d'autres WinAPI, ainsi que du multithreading sous Linux et d'autres systèmes * nix utilisant des pthreads. Si vous écrivez en C ++ 11 ou version ultérieure, alors std :: thread et d'autres primitives multithread introduites dans cette norme de langage sont à votre disposition. Ce qui suit vous montrera comment travailler avec eux. Contrairement à WinAPI et aux pthreads, le code écrit dans std :: thread est multiplateforme.

Noter: Le code ci-dessus a été testé sur GCC 7.1 et Clang 4.0 sous Arch Linux, GCC 5.4 et Clang 3.8 sous Ubuntu 16.04 LTS, GCC 5.4 et Clang 3.8 sous FreeBSD 11, et Visual Studio Community 2017 sous Windows 10. CMake ne peut pas parler avant la version 3.8 le compilateur doit utiliser la norme C ++ 17 spécifiée dans les propriétés du projet. Comment installer CMake 3.8 sur Ubuntu 16.04. Pour que le code soit compilé avec Clang, le package libc ++ doit être installé sur les systèmes * nix. Pour Arch Linux, le package est disponible sur l'AUR. Il existe un package libc ++ - dev dans Ubuntu, mais vous pouvez rencontrer des problèmes qui empêchent la construction du code si facilement. La solution de contournement est décrite sur StackOverflow. Sur FreeBSD, vous devez installer le paquet cmake-modules pour compiler le projet.

Mutex

Vous trouverez ci-dessous l'exemple le plus simple d'utilisation des transactions et des mutex :

#comprendre
#comprendre
#comprendre
#comprendre

Std :: mutex mtx;
compteur int statique = 0 ;


pour (;;) (
{
std :: lock_guard< std:: mutex >serrure (mtx);

Pause;
int ctr_val = ++ compteur ;
std :: cout<< "Thread " << tnum << ": counter = " <<
ctr_val<< std:: endl ;
}

}
}

int main () (
std :: vecteur< std:: thread >fils;
pour (int i = 0; i< 10 ; i++ ) {


}

// ne peut pas utiliser const auto & ici puisque .join () n'est pas marqué const

thr.join ();
}

Std :: cout<< "Done!" << std:: endl ;
renvoie 0 ;
}

Notez l'encapsulation de std :: mutex dans std :: lock_guard selon l'idiome RAII. Cette approche garantit que le mutex est libéré lorsque la portée se termine dans tous les cas, y compris lorsque des exceptions sont levées. Pour capturer plusieurs mutex à la fois afin d'éviter les blocages, il existe la classe std :: scoped_lock. Cependant, il n'est apparu qu'en C++ 17 et peut donc ne pas fonctionner partout. Pour les versions antérieures de C ++, il existe un modèle de verrou std :: de fonctionnalité similaire, bien qu'il nécessite l'écriture de code supplémentaire pour libérer correctement les verrous à l'aide de RAII.

Verrouillage RW

Souvent, une situation se présente dans laquelle l'accès à un objet est plus souvent en lecture qu'en écriture. Dans ce cas, au lieu du mutex habituel, il est plus efficace d'utiliser un verrou en lecture-écriture, alias RWLock. RWLock peut être capturé par plusieurs threads à la fois pour la lecture, ou par un seul thread pour l'écriture. RWLock en C++ correspond aux classes std :: shared_mutex et std :: shared_timed_mutex :

#comprendre
#comprendre
#comprendre
#comprendre

// std :: shared_mutex mtx; // ne fonctionnera pas avec GCC 5.4
std :: shared_timed_mutex mtx;

compteur int statique = 0 ;
statique const int MAX_COUNTER_VAL = 100 ;

void thread_proc (int tnum) (
pour (;;) (
{
// voir aussi std :: shared_lock
std :: verrou_unique< std:: shared_timed_mutex >serrure (mtx);
si (compteur == MAX_COUNTER_VAL)
Pause;
int ctr_val = ++ compteur ;
std :: cout<< "Thread " << tnum << ": counter = " <<
ctr_val<< std:: endl ;
}
std :: this_thread :: sleep_for (std :: chrono :: millisecondes (10));
}
}

int main () (
std :: vecteur< std:: thread >fils;
pour (int i = 0; i< 10 ; i++ ) {
std :: thread thr (thread_proc, i);
threads.emplace_back (std :: déplacer (thr));
}

pour (auto & thr : threads) (
thr.join ();
}

Std :: cout<< "Done!" << std:: endl ;
renvoie 0 ;
}

Par analogie avec std :: lock_guard, les classes std :: unique_lock et std :: shared_lock sont utilisées pour capturer RWLock, selon la façon dont nous voulons capturer le verrou. La classe std :: shared_timed_mutex est apparue en C++ 14 et fonctionne sur toutes les * plateformes modernes (je ne dirai pas pour les appareils mobiles, les consoles de jeux, etc.). Contrairement à std :: shared_mutex, il a try_lock_for, try_lock_unti et d'autres méthodes qui essaient de verrouiller le mutex pendant un temps donné. Je soupçonne fortement que std :: shared_mutex devrait être moins cher que std :: shared_timed_mutex. Cependant, std :: shared_mutex n'est apparu qu'en C ++ 17, ce qui signifie qu'il n'est pas pris en charge partout. En particulier, le GCC 5.4 encore largement utilisé ne le sait pas.

Stockage local des threads

Parfois, vous devez créer une variable, comme une variable globale, mais qu'un seul thread voit. D'autres threads voient également la variable, mais ils ont leur propre signification locale. Pour ce faire, ils ont proposé Thread Local Storage, ou TLS (n'a rien à voir avec Transport Layer Security !). Entre autres choses, TLS peut être utilisé pour accélérer considérablement la génération de nombres pseudo-aléatoires. Un exemple d'utilisation de TLS en C++ :

#comprendre
#comprendre
#comprendre
#comprendre

Std :: mutex io_mtx;
thread_local int compteur = 0 ;
statique const int MAX_COUNTER_VAL = 10 ;

void thread_proc (int tnum) (
pour (;;) (
compteur ++;
si (compteur == MAX_COUNTER_VAL)
Pause;
{
std :: lock_guard< std:: mutex >verrou (io_mtx);
std :: cout<< "Thread " << tnum << ": counter = " <<
contrer<< std:: endl ;
}
std :: this_thread :: sleep_for (std :: chrono :: millisecondes (10));
}
}

int main () (
std :: vecteur< std:: thread >fils;
pour (int i = 0; i< 10 ; i++ ) {
std :: thread thr (thread_proc, i);
threads.emplace_back (std :: déplacer (thr));
}

pour (auto & thr : threads) (
thr.join ();
}

Std :: cout<< "Done!" << std:: endl ;
renvoie 0 ;
}

Le mutex est utilisé ici uniquement pour synchroniser la sortie vers la console. Aucune synchronisation n'est requise pour accéder aux variables thread_local.

Variables atomiques

Les variables atomiques sont souvent utilisées pour effectuer des opérations simples sans utiliser de mutex. Par exemple, vous devez incrémenter un compteur à partir de plusieurs threads. Au lieu d'envelopper int dans std :: mutex, il est plus efficace d'utiliser std :: atomic_int. C++ propose également les types std :: atomic_char, std :: atomic_bool, et bien d'autres. Des algorithmes et des structures de données sans verrouillage sont également implémentés à l'aide de variables atomiques. Il convient de noter qu'ils sont très difficiles à développer et à déboguer, et que tous les systèmes ne fonctionnent pas plus rapidement que des algorithmes et des structures de données similaires avec des verrous.

Exemple de code :

#comprendre
#comprendre
#comprendre
#comprendre
#comprendre

static std :: atomic_int atomic_counter (0);
statique const int MAX_COUNTER_VAL = 100 ;

Std :: mutex io_mtx;

void thread_proc (int tnum) (
pour (;;) (
{
int ctr_val = ++ atomic_counter;
si (ctr_val> = MAX_COUNTER_VAL)
Pause;

{
std :: lock_guard< std:: mutex >verrou (io_mtx);
std :: cout<< "Thread " << tnum << ": counter = " <<
ctr_val<< std:: endl ;
}
}
std :: this_thread :: sleep_for (std :: chrono :: millisecondes (10));
}
}

int main () (
std :: vecteur< std:: thread >fils;

int nthreads = std :: thread :: hardware_concurrency ();
si (nthreads == 0) nthreads = 2 ;

pour (int i = 0; i< nthreads; i++ ) {
std :: thread thr (thread_proc, i);
threads.emplace_back (std :: déplacer (thr));
}

pour (auto & thr : threads) (
thr.join ();
}

Std :: cout<< "Done!" << std:: endl ;
renvoie 0 ;
}

Notez l'utilisation de la procédure hardware_concurrency. Il renvoie une estimation du nombre de threads pouvant être exécutés en parallèle sur le système actuel. Par exemple, sur une machine avec un processeur quadricœur qui prend en charge l'hyper threading, la procédure renvoie 8. En outre, la procédure peut renvoyer zéro si l'évaluation a échoué ou si la procédure n'a tout simplement pas été implémentée.

Pour plus d'informations sur le fonctionnement des variables atomiques au niveau de l'assembleur, consultez l'article Aide-mémoire pour les instructions d'assemblage de base x86 / x64.

Conclusion

D'après ce que je peux voir, tout cela fonctionne très bien. Autrement dit, lors de l'écriture d'applications multiplateformes en C ++, vous pouvez oublier WinAPI et pthreads en toute sécurité. En C pur, des échanges multiplateformes existent également depuis le C11. Mais ils ne sont toujours pas pris en charge par Visual Studio (j'ai vérifié) et il est peu probable qu'ils le soient un jour. Ce n'est un secret pour personne que Microsoft ne voit aucun intérêt à développer le support du langage C dans son compilateur, préférant se concentrer sur le C++.

Il y a encore beaucoup de primitives dans les coulisses : std :: condition_variable (_any), std: :(​shared_) future, std :: promise, std :: sync et autres. Je recommande cppreference.com pour les examiner. Il peut également être judicieux de lire le livre C ++ Concurrency in Action. Mais je dois vous prévenir qu'il n'est plus nouveau, contient beaucoup d'eau, et raconte en fait une dizaine d'articles de cppreference.com.

Le code source complet de cette note, comme d'habitude, se trouve sur GitHub. Comment écrivez-vous des applications C++ multithread maintenant ?

Clay Breshears

introduction

Les méthodes de mise en œuvre du multithreading d'Intel comprennent quatre phases principales : analyse, conception et mise en œuvre, débogage et optimisation des performances. C'est l'approche utilisée pour créer une application multi-thread à partir de code séquentiel. Le travail avec un logiciel au cours des première, troisième et quatrième étapes est assez largement couvert, tandis que les informations sur la mise en œuvre de la deuxième étape sont clairement insuffisantes.

De nombreux livres ont été publiés sur les algorithmes parallèles et le calcul parallèle. Cependant, ces publications couvrent principalement le passage de messages, les systèmes de mémoire distribuée ou les modèles théoriques de calcul parallèle qui sont parfois inapplicables à de véritables plates-formes multicœurs. Si vous êtes prêt à vous lancer sérieusement dans la programmation multithread, vous aurez probablement besoin de connaissances sur le développement d'algorithmes pour ces modèles. Bien entendu, l'utilisation de ces modèles est plutôt limitée, de sorte que de nombreux développeurs de logiciels peuvent être amenés à les mettre en œuvre dans la pratique.

Il n'est pas exagéré de dire que le développement d'applications multithread est d'abord une activité créative, et seulement ensuite une activité scientifique. Dans cet article, vous découvrirez huit règles simples pour vous aider à élargir votre base de pratiques de programmation simultanée et à améliorer l'efficacité du threading de vos applications.

Règle 1. Sélectionnez les opérations effectuées dans le code du programme indépendamment les unes des autres

Le traitement parallèle s'applique uniquement aux opérations dans le code séquentiel qui sont effectuées indépendamment les unes des autres. Un bon exemple de la façon dont des actions indépendantes conduisent à un résultat unique réel est la construction d'une maison. Il implique des ouvriers de nombreuses spécialités : menuisiers, électriciens, plâtriers, plombiers, couvreurs, peintres, maçons, jardiniers, etc. Bien entendu, certains d'entre eux ne peuvent pas commencer à travailler avant que d'autres n'aient terminé leurs activités (par exemple, les couvreurs ne commenceront à travailler qu'une fois les murs construits, et les peintres ne peindront pas ces murs s'ils ne sont pas enduits). Mais en général, on peut dire que toutes les personnes impliquées dans la construction agissent indépendamment les unes des autres.

Prenons un autre exemple - le cycle de travail d'un magasin de location de DVD qui reçoit des commandes pour certains films. Les commandes sont réparties entre les employés du point qui recherchent ces films dans l'entrepôt. Naturellement, si l'un des ouvriers sort un disque de l'entrepôt sur lequel a été enregistré un film avec Audrey Hepburn, cela n'affectera en rien un autre ouvrier à la recherche d'un autre film d'action avec Arnold Schwarzenegger, et encore plus n'affectera pas son collègue, qui est à la recherche de disques avec la nouvelle saison de la série "Friends". Dans notre exemple, nous pensons que tous les problèmes liés au manque de films en stock ont ​​été résolus avant l'arrivée des commandes au point de location, et l'emballage et l'expédition de toute commande n'affecteront pas le traitement des autres.

Dans votre travail, vous rencontrerez probablement des calculs qui ne peuvent être traités que dans une séquence spécifique, et non en parallèle, car les différentes itérations ou étapes de la boucle dépendent les unes des autres et doivent être effectuées dans un ordre strict. Prenons un exemple vivant de la nature. Imaginez une biche enceinte. Puisque porter un fœtus dure en moyenne huit mois, alors, quoi qu'on en dise, un faon n'apparaîtra pas dans un mois, même si huit rennes tombent enceintes en même temps. Cependant, huit rennes en même temps feraient parfaitement leur travail s'ils étaient tous attelés à tous dans le traîneau du Père Noël.

Règle 2. Appliquer le parallélisme avec un faible niveau de détail

Il existe deux approches pour le partitionnement parallèle du code de programme séquentiel : ascendant et descendant. Tout d'abord, au stade de l'analyse du code, sont déterminés des segments de code (appelés "points chauds") qui occupent une part importante du temps d'exécution du programme. La séparation de ces segments de code en parallèle (si possible) fournira le gain de performances maximal.

L'approche ascendante implémente un traitement multithread des points chauds du code. Si le fractionnement parallèle des points trouvés n'est pas possible, vous devez examiner la pile d'appels de l'application pour déterminer les autres segments disponibles pour le fractionnement parallèle et qui prennent beaucoup de temps. Disons que vous travaillez sur une application de compression de graphiques. La compression peut être mise en œuvre à l'aide de plusieurs flux parallèles indépendants qui traitent des segments individuels de l'image. Cependant, même si vous avez réussi à implémenter des "hotspots" multithreads, ne négligez pas l'analyse de la pile d'appels, à la suite de laquelle vous pouvez trouver des segments disponibles pour le fractionnement parallèle à un niveau supérieur du code du programme. De cette façon, vous pouvez augmenter la granularité du traitement parallèle.

Dans l'approche descendante, le travail du code du programme est analysé et ses segments individuels sont mis en évidence, dont l'exécution conduit à l'achèvement de l'ensemble de la tâche. S'il n'y a pas d'indépendance claire des principaux segments de code, analysez leurs éléments constitutifs pour trouver des calculs indépendants. En analysant le code du programme, vous pouvez déterminer les modules de code qui consomment le plus de temps CPU. Voyons comment implémenter le threading dans une application d'encodage vidéo. Le traitement parallèle peut être mis en œuvre au niveau le plus bas - pour les pixels indépendants d'une trame, ou à un niveau supérieur - pour des groupes de trames pouvant être traités indépendamment des autres groupes. Si une application est écrite pour traiter plusieurs fichiers vidéo en même temps, le fractionnement parallèle à ce niveau peut être encore plus facile et les détails seront les plus bas.

La granularité du calcul parallèle fait référence à la quantité de calcul qui doit être effectuée avant la synchronisation entre les threads. En d'autres termes, moins la synchronisation est fréquente, plus la granularité est faible. Les calculs de threads à granularité fine peuvent entraîner une surcharge système du threading qui dépasse les calculs utiles effectués par ces threads. L'augmentation du nombre de threads avec la même quantité de calcul complique le processus de traitement. Le multithreading à faible granularité introduit moins de latence du système et a plus de potentiel d'évolutivité, ce qui peut être réalisé avec des threads supplémentaires. Pour mettre en œuvre un traitement parallèle à faible granularité, il est recommandé d'utiliser une approche descendante et un thread à un niveau élevé dans la pile des appels.

Règle 3. Intégrez l'évolutivité dans votre code pour améliorer les performances à mesure que le nombre de cœurs augmente.

Il n'y a pas si longtemps, en plus des processeurs dual-core, des processeurs quad-core sont apparus sur le marché. De plus, Intel a déjà annoncé un processeur à 80 cœurs, capable d'effectuer mille milliards d'opérations en virgule flottante par seconde. Étant donné que le nombre de cœurs dans les processeurs ne fera qu'augmenter avec le temps, votre code doit avoir un potentiel d'évolutivité adéquat. L'évolutivité est un paramètre par lequel on peut juger de la capacité d'une application à répondre de manière adéquate à des changements tels qu'une augmentation des ressources système (nombre de cœurs, taille de la mémoire, fréquence du bus, etc.) ou une augmentation de la quantité de données. Avec l'augmentation du nombre de cœurs dans les futurs processeurs, écrivez du code évolutif qui augmentera les performances en augmentant les ressources système.

Pour paraphraser l'une des lois de C. Northecote Parkinson, on peut dire que "le traitement des données utilise toutes les ressources système disponibles". Cela signifie qu'à mesure que les ressources informatiques augmentent (par exemple, le nombre de cœurs), toutes sont plus susceptibles d'être utilisées pour traiter des données. Revenons à l'application de compression vidéo évoquée ci-dessus. Il est peu probable que l'ajout de cœurs supplémentaires au processeur affecte la taille des trames traitées - au lieu de cela, le nombre de threads traitant la trame augmentera, ce qui entraînera une diminution du nombre de pixels par flux. En conséquence, en raison de l'organisation de flux supplémentaires, la quantité de données de service augmentera et le degré de granularité du parallélisme diminuera. Un autre scénario plus probable est une augmentation de la taille ou du nombre de fichiers vidéo qui doivent être encodés. Dans ce cas, l'organisation de flux supplémentaires qui traiteront des fichiers vidéo plus volumineux (ou supplémentaires) permettra de diviser tout le volume de travail directement au stade où l'augmentation a eu lieu. À son tour, une application dotée de telles capacités aura un potentiel élevé d'évolutivité.

La conception et la mise en œuvre du traitement parallèle à l'aide de la décomposition des données offrent une évolutivité accrue par rapport à l'utilisation de la décomposition fonctionnelle. Le nombre de fonctions indépendantes dans le code du programme est le plus souvent limité et ne change pas au cours de l'exécution de l'application. Étant donné que chaque fonction indépendante se voit attribuer un thread séparé (et, par conséquent, un cœur de processeur), alors avec une augmentation du nombre de cœurs, des threads organisés en plus n'entraîneront pas d'augmentation des performances. Ainsi, les modèles de partitionnement parallèle avec décomposition des données offriront un potentiel accru d'évolutivité de l'application en raison du fait que la quantité de données traitées augmentera avec le nombre de cœurs de processeur.

Même si le code du programme enchaîne des fonctions indépendantes, il est possible d'utiliser des threads supplémentaires qui sont lancés lorsque la charge d'entrée augmente. Revenons à l'exemple de construction de maison discuté ci-dessus. Le but particulier de la construction est d'accomplir un nombre limité de tâches indépendantes. Cependant, si on vous demande de construire deux fois plus d'étages, vous souhaiterez probablement embaucher des ouvriers supplémentaires dans certaines spécialités (peintres, couvreurs, plombiers, etc.). Par conséquent, vous devez développer des applications capables de s'adapter à la décomposition des données résultant d'une charge de travail accrue. Si votre code implémente la décomposition fonctionnelle, envisagez d'organiser des threads supplémentaires à mesure que le nombre de cœurs de processeur augmente.

Règle 4. Utiliser des bibliothèques thread-safe

Si vous avez besoin d'une bibliothèque pour gérer les points chauds de données dans votre code, pensez à utiliser des fonctions prêtes à l'emploi au lieu de votre propre code. Bref, n'essayez pas de réinventer la roue en développant des segments de code dont les fonctions sont déjà fournies dans des procédures optimisées de la bibliothèque. De nombreuses bibliothèques, dont Intel® Math Kernel Library (Intel® MKL) et Intel® Integrated Performance Primitives (Intel® IPP), contiennent déjà des fonctionnalités multithread optimisées pour les processeurs multicœurs.

Il convient de noter que lors de l'utilisation de procédures à partir des bibliothèques multithread, vous devez vous assurer que l'appel de l'une ou l'autre bibliothèque n'affectera pas le fonctionnement normal des threads. C'est-à-dire que si des appels de procédure sont effectués à partir de deux threads différents, des résultats corrects doivent être renvoyés à partir de chaque appel. Si les procédures font référence à des variables de bibliothèque partagée et les mettent à jour, une course aux données peut se produire, ce qui affectera négativement la fiabilité des résultats de calcul. Pour fonctionner correctement avec les threads, la procédure de bibliothèque est ajoutée en tant que nouvelle (c'est-à-dire qu'elle ne met à jour rien d'autre que les variables locales) ou synchronisée pour protéger l'accès aux ressources partagées. Conclusion : avant d'utiliser une bibliothèque tierce dans le code de votre programme, lisez la documentation qui y est attachée pour vous assurer qu'elle fonctionne correctement avec les flux.

Règle 5. Utilisez un modèle de multithreading approprié

Supposons que les fonctions des bibliothèques multithread ne soient clairement pas suffisantes pour la division parallèle de tous les segments de code appropriés, et que vous deviez penser à l'organisation des threads. Ne vous précipitez pas pour créer votre propre structure de threads (encombrante) si la bibliothèque OpenMP contient déjà toutes les fonctionnalités dont vous avez besoin.

L'inconvénient du multithreading explicite est l'impossibilité d'un contrôle précis des threads.

Si vous n'avez besoin que d'une séparation parallèle des boucles gourmandes en ressources, ou si la flexibilité supplémentaire fournie par les threads explicites est secondaire pour vous, alors dans ce cas, cela n'a aucun sens de faire un travail supplémentaire. Plus la mise en œuvre du multithreading est complexe, plus la probabilité d'erreurs dans le code est grande et plus sa révision ultérieure est difficile.

La bibliothèque OpenMP se concentre sur la décomposition des données et est particulièrement bien adaptée pour les boucles de thread travaillant avec de grandes quantités d'informations. Malgré le fait que seule la décomposition des données soit applicable à certaines applications, il est nécessaire de prendre en compte des exigences supplémentaires (par exemple, de l'employeur ou du client), selon lesquelles l'utilisation d'OpenMP est inacceptable et il reste à mettre en œuvre le multithreading à l'aide méthodes. Dans ce cas, OpenMP peut être utilisé pour le threading préliminaire afin d'estimer les gains de performances potentiels, l'évolutivité et l'effort approximatif qui seraient nécessaires pour diviser par la suite le code par multithreading explicite.

Règle 6. Le résultat du code du programme ne doit pas dépendre de la séquence d'exécution des threads parallèles

Pour le code de programme séquentiel, il suffit de définir simplement une expression qui sera exécutée après toute autre expression. Dans le code multi-thread, l'ordre d'exécution des threads n'est pas défini et dépend des instructions de l'ordonnanceur du système d'exploitation. À proprement parler, il est presque impossible de prédire la séquence de threads qui seront lancées pour effectuer une opération, ou de déterminer quel thread sera lancé ultérieurement par le planificateur. La prédiction est principalement utilisée pour réduire la latence d'une application, en particulier lorsqu'elle s'exécute sur une plate-forme avec un processeur avec moins de cœurs que le nombre de threads organisés. Si un thread est bloqué parce qu'il a besoin d'accéder à une zone non écrite dans le cache, ou parce qu'il a besoin d'exécuter une requête d'E/S, le planificateur le suspendra et démarrera le thread prêt à démarrer.

Les situations de concurrence entre les données sont le résultat immédiat de l'incertitude dans la planification de l'exécution des threads. Il peut être faux de supposer qu'un thread changera la valeur d'une variable partagée avant qu'un autre thread ne lise cette valeur. Avec un peu de chance, l'ordre d'exécution des threads pour une plate-forme particulière restera le même à tous les lancements de l'application. Cependant, les moindres changements dans l'état du système (par exemple, l'emplacement des données sur le disque dur, la vitesse de la mémoire, ou encore un écart par rapport à la fréquence nominale du réseau d'alimentation alternatif) peuvent provoquer un ordre différent de exécution de threads. Ainsi, pour le code de programme qui ne fonctionne correctement qu'avec une certaine séquence de threads, des problèmes associés aux situations de « course aux données » et aux blocages sont probables.

Du point de vue gain de performances, il est préférable de ne pas restreindre l'ordre d'exécution des threads. Une séquence stricte d'exécution des flux n'est autorisée qu'en cas d'urgence, déterminée par un critère prédéterminé. Dans une telle circonstance, les threads seront lancés dans l'ordre spécifié par les mécanismes de synchronisation fournis. Par exemple, imaginez deux amis lisant un journal étalé sur une table. Premièrement, ils peuvent lire à différentes vitesses, et deuxièmement, ils peuvent lire différents articles. Et ici, peu importe qui lit en premier la diffusion du journal - de toute façon, il devra attendre son ami avant de tourner la page. Dans le même temps, il n'y a aucune restriction sur l'heure et l'ordre de lecture des articles - les amis lisent à n'importe quelle vitesse et la synchronisation entre eux se produit immédiatement lorsque vous tournez la page.

Règle 7. Utilisez le stockage de flux local. Attribuez des verrous à des zones de données spécifiques selon les besoins

La synchronisation augmente inévitablement la charge sur le système, ce qui n'accélère en rien le processus d'obtention des résultats des calculs parallèles, mais garantit leur exactitude. Oui, la synchronisation est nécessaire, mais il ne faut pas en abuser. Pour minimiser la synchronisation, un stockage local de flux ou de zones mémoire allouées (par exemple, des éléments de tableau marqués d'identifiants des flux correspondants) est utilisé.

Le besoin de partager des variables temporaires par différents threads est rare. De telles variables doivent être déclarées ou allouées localement à chaque thread. Les variables dont les valeurs sont des résultats intermédiaires de l'exécution des threads doivent également être déclarées locales aux threads correspondants. Une synchronisation est nécessaire pour additionner ces résultats intermédiaires dans une zone de mémoire partagée. Pour minimiser le stress potentiel sur le système, il est préférable de mettre à jour le moins possible cet espace commun. Pour les méthodes multithread explicites, il existe des API de stockage local de thread qui assurent l'intégrité des données locales depuis le début de l'exécution d'un segment de code multithread jusqu'au début du segment suivant (ou pendant le traitement d'un appel à une fonction multithread jusqu'au suivant l'exécution de la même fonction).

S'il n'est pas possible de stocker les flux localement, l'accès aux ressources partagées est synchronisé à l'aide de divers objets, tels que des verrous. Dans ce cas, il est important d'affecter correctement des verrous à des blocs de données spécifiques, ce qui est plus facile à faire si le nombre de verrous est égal au nombre de blocs de données. Un mécanisme de verrouillage unique qui synchronise l'accès à plusieurs zones de mémoire n'est utilisé que lorsque toutes ces zones se trouvent constamment dans la même section critique du code du programme.

Que faire si vous devez synchroniser l'accès à une grande quantité de données, par exemple à un tableau de 10 000 éléments ? Fournir un seul verrou pour l'ensemble de la baie est certainement un goulot d'étranglement dans l'application. Faut-il vraiment organiser le verrouillage de chaque élément séparément ? Ensuite, même si 32 ou 64 threads parallèles accèdent aux données, vous devrez éviter les conflits d'accès à une zone mémoire assez importante, et la probabilité de tels conflits est de 1%. Heureusement, il existe une sorte de juste milieu, ce qu'on appelle les « verrous modulo ». Si N verrous modulo sont utilisés, chacun synchronisera l'accès à la Nième partie de la zone de données partagée. Par exemple, si deux de ces verrous sont organisés, l'un d'eux empêchera l'accès aux éléments pairs du tableau et l'autre aux éléments impairs. Dans ce cas, les threads, se référant à l'élément requis, déterminent sa parité et définissent le verrou approprié. Le nombre de verrous modulo est choisi en tenant compte du nombre de threads et de la probabilité d'accès simultané de plusieurs threads à la même zone mémoire.

A noter que l'utilisation simultanée de plusieurs mécanismes de verrouillage n'est pas autorisée pour synchroniser l'accès à une zone mémoire. Rappelons la loi de Segal : « Une personne qui a une montre sait avec certitude quelle heure il est. Une personne qui a quelques montres n'est sûre de rien." Supposons que deux verrous différents contrôlent l'accès à une variable. Dans ce cas, le premier verrou peut être utilisé par un segment du code, et le second par un autre segment. Ensuite, les threads exécutant ces segments se retrouveront dans une situation de course pour les données partagées auxquelles ils accèdent en même temps.

Règle 8. Modifier l'algorithme du logiciel si nécessaire pour implémenter le multithreading

Le critère d'évaluation des performances des applications, tant séquentielles que parallèles, est le temps d'exécution. Comme estimation de l'algorithme, un ordre asymptotique convient. Cette métrique théorique est presque toujours utile pour évaluer les performances d'une application. Autrement dit, toutes choses étant égales par ailleurs, une application avec un taux de croissance de O (n log n) (tri rapide) s'exécutera plus rapidement qu'une application avec un taux de croissance de O (n2) (tri sélectif), bien que les résultats de ces les candidatures sont les mêmes.

Plus l'ordre d'exécution asymptotique est bon, plus l'application parallèle s'exécute rapidement. Cependant, même l'algorithme séquentiel le plus efficace ne peut pas toujours être divisé en flux parallèles. Si le point chaud d'un programme est trop difficile à diviser et qu'il n'y a aucun moyen de multithread à un niveau supérieur de la pile d'appels du point chaud, vous devez d'abord envisager d'utiliser un algorithme séquentiel différent qui est plus facile à diviser que l'original. Bien sûr, il existe d'autres façons de préparer votre code pour le threading.

Comme illustration du dernier énoncé, considérons la multiplication de deux matrices carrées. L'algorithme de Strassen possède l'un des meilleurs ordres d'exécution asymptotique : O (n2.81), qui est bien meilleur que l'ordre O (n3) de l'algorithme ordinaire à triple boucle imbriquée. Selon l'algorithme de Strassen, chaque matrice est divisée en quatre sous-matrices, après quoi sept appels récursifs sont effectués pour multiplier n/2 × n/2 sous-matrices. Pour paralléliser les appels récursifs, vous pouvez créer un nouveau thread qui effectuera séquentiellement sept multiplications indépendantes de sous-matrices jusqu'à ce qu'elles atteignent la taille spécifiée. Dans ce cas, le nombre de threads augmentera de manière exponentielle et la granularité des calculs effectués par chaque thread nouvellement formé augmentera avec la diminution de la taille des sous-matrices. Considérons une autre option - organiser un pool de sept threads travaillant simultanément et effectuant une multiplication de sous-matrices. À la fin du pool de threads, la méthode Strassen est appelée de manière récursive pour multiplier les sous-matrices (comme dans la version séquentielle du code du programme). Si le système exécutant un tel programme a plus de huit cœurs de processeur, certains d'entre eux seront inactifs.

L'algorithme de multiplication matricielle est beaucoup plus facile à paralléliser en utilisant une boucle ternaire imbriquée. Dans ce cas, une décomposition des données est appliquée, dans laquelle les matrices sont divisées en lignes, colonnes ou sous-matrices, et chacun des threads effectue certains calculs. La mise en œuvre d'un tel algorithme est réalisée à l'aide de pragmas OpenMP insérés à un certain niveau de la boucle, ou en organisant explicitement des threads qui effectuent une division matricielle. La mise en œuvre de cet algorithme séquentiel plus simple nécessitera beaucoup moins de modifications dans le code du programme, par rapport à la mise en œuvre de l'algorithme de Strassen multithread.

Ainsi, vous connaissez maintenant huit règles simples pour convertir efficacement du code séquentiel en parallèle. En suivant ces directives, vous serez en mesure de créer des solutions multithread beaucoup plus rapidement, avec une fiabilité accrue, des performances optimales et moins de goulots d'étranglement.

Pour revenir à la page Web des didacticiels de programmation multithread, accédez à

Andreï Kolesov

Pour commencer à examiner les principes de création d'applications multithread pour Microsoft .NET Framework, faisons une réserve : bien que tous les exemples soient donnés en Visual Basic .NET, la méthodologie de création de tels programmes est généralement la même pour tous les langages de programmation qui prennent en charge .NET, y compris C#. VB a été choisi pour démontrer la technologie de création d'applications multithread principalement parce que les versions précédentes de cet outil n'offraient pas une telle opportunité.

Attention : Visual Basic .NET peut le faire aussi !

Comme vous le savez, Visual Basic (jusqu'à la version 6.0 incluse) n'a jamais autorisé la création de composants logiciels multithread (EXE, DLL ActiveX et OCX) auparavant. N'oubliez pas que l'architecture COM comprend trois modèles de threads différents : Single Thread, Single Threaded Apartment (STA) et Multi-Threaded Apartment. VB 6.0 vous permet de créer des programmes des deux premiers types. La version STA prévoit un mode pseudo-multithreading - plusieurs threads fonctionnent vraiment en parallèle, mais en même temps le code du programme de chacun d'eux est protégé contre tout accès de l'extérieur (en particulier, les threads ne peuvent pas utiliser de ressources partagées).

Visual Basic .NET peut désormais implémenter le threading gratuit dans sa forme native. Plus précisément, en .NET, ce mode est supporté au niveau des bibliothèques de classes communes Class Library et Common Language Runtime. En conséquence, VB.NET a eu accès à ces capacités ainsi qu'à d'autres langages de programmation .NET.

À un moment donné, la communauté des développeurs VB, exprimant son mécontentement face à de nombreuses innovations futures de ce langage, a réagi avec une grande approbation à l'annonce qu'avec l'aide de la nouvelle version de l'outil, il sera possible de créer des programmes multithread (voir "En attente de Visual Studio .NET", "BYTE / Russie "N° 1/2001). Cependant, de nombreux experts ont exprimé des appréciations plus modérées de cette innovation. Par exemple, voici l'opinion de Dan Appleman, un développeur bien connu et auteur de nombreux livres pour les programmeurs VB : en raison de facteurs humains plutôt que technologiques... J'ai peur du multithreading dans VB.NET car les programmeurs VB ne le font généralement pas. avoir de l'expérience dans la conception et le débogage d'applications multithread. "

En effet, comme d'autres outils de programmation de bas niveau (par exemple, les mêmes API Win), le multithreading gratuit, d'une part, offre plus d'opportunités pour créer des solutions évolutives hautes performances, et d'autre part, il impose des exigences plus élevées aux utilisateurs. qualifications des développeurs. De plus, le problème est ici aggravé par le fait que la recherche d'erreurs dans une application multithread est très difficile, puisqu'elles apparaissent le plus souvent de manière aléatoire, à la suite d'une intersection spécifique de processus de calcul parallèles (il est souvent tout simplement impossible de reproduire de tels une situation à nouveau). C'est pourquoi les méthodes traditionnelles de débogage des programmes sous la forme de leur exécution répétée n'aident généralement pas dans ce cas. Et la seule façon d'utiliser le multithreading en toute sécurité est de bien concevoir votre application, en respectant tous les principes classiques d'une "bonne programmation".

Le problème avec les programmeurs VB est que bien que beaucoup d'entre eux soient des professionnels assez expérimentés et bien conscients des pièges du multithreading, l'utilisation de VB6 pourrait émousser leur vigilance. Après tout, accusant VB de limitations, nous oublions parfois que de nombreuses limitations ont été déterminées par les fonctionnalités de sécurité améliorées de cet outil, qui empêchent ou éliminent les erreurs de développement. Par exemple, VB6 crée automatiquement une copie séparée de toutes les variables globales pour chaque thread, évitant ainsi d'éventuels conflits entre elles. Dans VB.NET, ces problèmes sont complètement transférés sur les épaules du programmeur. Il faut également se rappeler que l'utilisation d'un modèle multithread au lieu d'un modèle monothread ne conduit pas toujours à une augmentation des performances du programme ; les performances peuvent même diminuer (même dans les systèmes multiprocesseurs !).

Cependant, tout ce qui précède ne doit pas être considéré comme un conseil de ne pas jouer avec le multithreading. Vous avez juste besoin d'avoir une bonne idée du moment où de tels modes valent la peine d'être utilisés et de comprendre qu'un outil de développement plus puissant impose toujours des exigences plus élevées aux qualifications du programmeur.

Traitement parallèle en VB6

Bien sûr, il était possible d'organiser des traitements de données pseudo-parallèles à l'aide de VB6, mais ces possibilités étaient très limitées. Par exemple, il y a plusieurs années, j'avais besoin d'écrire une procédure qui suspend l'exécution d'un programme pendant un nombre spécifié de secondes (l'instruction SLEEP correspondante était facilement disponible dans Microsoft Basic / DOS). Il n'est pas difficile de l'implémenter vous-même sous la forme du sous-programme simple suivant :

Ses performances peuvent être facilement vérifiées, par exemple, en utilisant le code suivant pour gérer un clic sur un bouton sur un formulaire :

Pour résoudre ce problème dans VB6, à l'intérieur de la boucle Do... de la procédure SleepVB, vous devez décommenter l'appel à la fonction DoEvents, qui transfère le contrôle au système d'exploitation et renvoie le nombre de formulaires ouverts dans cette application VB. Mais notez que l'affichage d'une fenêtre avec le message "Another hello!", à son tour, bloque l'exécution de l'ensemble de l'application, y compris la procédure SleepVB.

En utilisant des variables globales comme indicateurs, vous pouvez également vous assurer que la procédure SleepVB en cours d'exécution peut se terminer de manière anormale. C'est à son tour l'exemple le plus simple d'un processus de calcul qui utilise complètement les ressources du processeur. Mais si vous effectuez des calculs utiles (et ne tournez pas dans une boucle vide), vous devez garder à l'esprit que l'appel à la fonction DoEvent prend assez de temps, donc cela doit être fait à des intervalles assez grands.

Pour voir la prise en charge limitée du calcul parallèle dans VB6, remplacez l'appel à la fonction DoEvents par une sortie d'étiquette :

Label1.Caption = Minuteur

Dans ce cas, non seulement le bouton Command2 ne sera pas déclenché, mais même dans les 5 secondes, le contenu de l'étiquette ne changera pas.

Pour une autre expérience, ajoutez un appel d'attente au code de Command2 (cela peut être fait puisque la procédure SleepVB est réentrante) :

Private Sub Command2_Click () Appelez SleepVB (5) MsgBox "Un autre bonjour !" Fin du sous-marin

Ensuite, lancez l'application et cliquez sur Command1 et après 2-3 secondes - Command2. Le premier message à apparaître est "Another hello!" Bien que le processus ait été lancé plus tard. En effet, la fonction DoEvents vérifie uniquement les événements sur les visuels, et non la présence d'autres threads de calcul. De plus, l'application VB s'exécute en fait dans un thread, de sorte que le contrôle est revenu à la procédure événementielle qui a été démarrée en dernier.

Contrôle des threads .NET

La création d'applications .NET multithread repose sur l'utilisation d'un groupe de classes de base .NET Framework décrites dans l'espace de noms System.Threading. Dans ce cas, le rôle clé appartient à la classe Thread, à l'aide de laquelle presque toutes les opérations de gestion des threads sont effectuées. À partir de ce moment, tout ce qui a été dit sur le travail avec les threads s'applique à tous les outils de programmation dans .NET, y compris C #.

Pour une première prise de conscience avec la création de flux parallèles, nous allons créer une application Windows avec un formulaire sur lequel nous allons placer les boutons ButtonStart et ButtonAbort et écrire le code suivant :

Je voudrais immédiatement attirer votre attention sur trois points. Premièrement, les mots-clés Imports sont utilisés pour faire référence aux noms abrégés des classes décrites ici par l'espace de noms. J'ai spécifiquement cité une autre utilisation des importations pour décrire l'équivalent abrégé d'un nom d'espace de noms long (VB = Microsoft.VisualBasic) qui peut être appliqué au texte du programme. Dans ce cas, vous pouvez immédiatement voir à quel espace de noms appartient l'objet Timer.

Deuxièmement, j'ai utilisé des crochets booléens #Region pour séparer visuellement le code que j'ai écrit du code généré automatiquement par le concepteur de formulaire (ce dernier n'est pas affiché ici).

Troisièmement, les descriptions des paramètres d'entrée des procédures événementielles ont été spécialement supprimées (cela sera parfois fait dans le futur), afin de ne pas se laisser distraire par des choses qui ne sont pas importantes dans ce cas.

Démarrez l'application et cliquez sur le bouton ButtonStart. Le processus d'attente dans une boucle pour un intervalle de temps spécifié a commencé, et dans ce cas (contrairement à l'exemple avec VB6) - dans un thread indépendant. C'est facile à voir - tous les éléments visuels du formulaire sont accessibles. Par exemple, en cliquant sur le bouton ButtonAbort, vous pouvez abandonner le processus à l'aide de la méthode Abort (mais fermer le formulaire à l'aide du bouton système Close n'annulera pas la procédure !). Pour plus de clarté sur la dynamique du processus, vous pouvez placer une étiquette sur le formulaire et ajouter la sortie de l'heure actuelle à la boucle d'attente de la procédure SleepVBNET :

Label1.Text = _ "Heure actuelle =" & VB.TimeOfDay

L'exécution de la procédure SleepVBNET (qui dans ce cas est déjà une méthode du nouvel objet) continuera même si vous ajoutez au code ButtonStart l'affichage d'une boîte de message sur le démarrage des calculs après le démarrage du thread (Fig. 1 ).

Une option plus complexe est un flux en tant que classe

Pour d'autres expériences avec les threads, créons une nouvelle application VB de type Console, composée d'un module de code régulier avec une procédure Main (qui commence à s'exécuter au démarrage de l'application) et un module de la classe WorkerThreadClass :

Lançons l'application créée. Une fenêtre de console apparaîtra, dans laquelle vous verrez une ligne de caractères déroulante montrant le modèle du processus de calcul en cours (WorkerThread). Ensuite, une boîte de message apparaîtra, émise par le processus appelant (Main), et enfin nous verrons l'image montrée dans la Fig. 2 (si vous n'êtes pas satisfait de la vitesse d'exécution du processus modélisé, supprimez ou ajoutez des opérations arithmétiques avec la variable "a" dans la procédure WorkerThread).

Attention : la boîte de message « Premier thread démarré » s'affichait avec un retard notable après le démarrage du processus WorkerThread (dans le cas du formulaire décrit dans le paragraphe précédent, un tel message apparaîtrait presque instantanément après avoir appuyé sur le bouton ButtonStart) . Cela est probablement dû au fait que les procédures événementielles sont prioritaires sur le processus démarré lors du travail sur le formulaire. Dans le cas d'une application console, toutes les procédures ont la même priorité. Nous discuterons de la question des priorités plus tard, mais pour l'instant, définissons le thread appelant (Main) sur la priorité la plus élevée :

Thread.CurrentThread.Priority = _ ThreadPriority.Highest Thread1.Start ()

Maintenant, la fenêtre apparaît presque immédiatement. Comme vous pouvez le voir, il existe deux manières de créer des instances de l'objet Thread. Tout d'abord, nous avons appliqué le premier d'entre eux - nous avons créé un nouvel objet (thread) Thread1 et travaillé avec. La deuxième option consiste à obtenir l'objet Thread pour le thread en cours d'exécution à l'aide de la méthode statique CurrentThread. C'est ainsi que la procédure Main se fixe une priorité plus élevée, mais elle pourrait le faire pour n'importe quel autre thread, par exemple :

Thread1.Priority = ThreadPriority.Lowest Thread1.Start ()

Pour montrer les possibilités de gestion d'un processus en cours d'exécution, ajoutez les lignes de code suivantes à la fin de la procédure Main :

Maintenant, démarrez l'application tout en effectuant quelques opérations de souris en même temps (j'espère que vous avez choisi le niveau de latence souhaité dans WorkerThread afin que le processus ne soit pas très rapide, mais pas trop lent non plus).

Tout d'abord, « Process 1 » démarre dans la fenêtre de la console et le message « Premier thread démarré » apparaît. "Process 1" est en cours d'exécution et vous cliquez rapidement sur le bouton OK dans la boîte de message.

Plus loin - « Process 1 » se poursuit, mais après deux secondes, le message « Le fil est suspendu » apparaît. Le processus 1 a gelé. Cliquez sur OK dans la boîte de message : Le processus 1 s'est poursuivi et s'est terminé avec succès.

Dans cet extrait, nous avons utilisé la méthode Sleep pour suspendre le processus en cours. Remarque : Sleep est une méthode statique et ne peut être appliquée qu'au processus en cours, pas à n'importe quelle instance de Thread. La syntaxe du langage permet d'écrire Thread1.Sleep ou Thread.Sleep, mais dans ce cas, l'objet CurrentThread est toujours utilisé.

La méthode Sleep peut également utiliser l'argument 0. Dans ce cas, le thread actuel libérera le reste inutilisé de sa tranche de temps allouée.

Un autre cas d'utilisation intéressant pour Sleep est avec une valeur Timeout.Infinite. Dans ce cas, le thread sera suspendu indéfiniment jusqu'à ce que l'état soit interrompu par un autre thread à l'aide de la méthode Thread.Interrupt.

Pour suspendre un thread externe à un autre thread sans arrêter ce dernier, vous devez utiliser un appel à la méthode Thread.Suspend. Ensuite, il sera possible de poursuivre son exécution par la méthode Thread.Resume, ce que nous avons fait dans le code ci-dessus.

Un peu sur la synchronisation des threads

La synchronisation des threads est l'une des principales préoccupations lors de l'écriture d'applications multithread, et l'espace System.Threading fournit une large gamme d'outils pour y parvenir. Mais maintenant, nous ne nous familiariserons qu'avec la méthode Thread.Join, qui permet de suivre la fin de l'exécution d'un thread. Pour voir comment cela fonctionne, remplacez les dernières lignes de Main par ce code :

Gestion des priorités de processus

L'allocation des tranches de temps processeur entre les threads est effectuée à l'aide de priorités, qui sont définies sous la forme de la propriété Thread.Priority. Les flux créés au moment de l'exécution peuvent être définis sur cinq valeurs : le plus élevé, au-dessus de la norme, normal (par défaut), en dessous de la norme et le plus bas. Pour voir comment les priorités affectent la vitesse d'exécution des threads, écrivons le code suivant pour la procédure Main :

Sub Main () "description du premier processus Dim Thread1 As Thread Dim oWorker1 As New WorkerThreadClass () Thread1 = New Thread (AddressOf _ oWorker1.WorkerThread)" Thread1.Priority = _ "ThreadPriority.BelowNormal" transfère les données initiales : oWorker1. Start = 1 oWorker1.Finish = 10 oWorker1.ThreadName = "Compte à rebours 1" oWorker1.SymThread = "." "description du deuxième processus Dim Thread2 As Thread Dim oWorker2 As New WorkerThreadClass () Thread2 = New Thread (AddressOf _ oWorker2.WorkerThread)" transfère les données initiales : oWorker2.Start = 11 oWorker2.Finish = 20 oWorker2.ThreadName = "Count 2" oWorker .SymThread = "*" "" démarrant une course Thread.CurrentThread.Priority = _ ThreadPriority.Highest Thread1.Start () Thread2.Start () "En attente de la fin des processus Thread1.Join () Thread2.Join ( ) MsgBox (" Les deux processus se sont terminés ") End Sub

Notez que cela utilise une seule classe pour créer plusieurs threads. Commençons l'application et regardons la dynamique d'exécution des deux threads (Fig. 3). Ici vous pouvez voir que, en général, ils sont exécutés à la même vitesse, le premier est légèrement en avance en raison du lancement plus tôt.

Maintenant, avant de démarrer le premier thread, définissez sa priorité un niveau plus bas :

Thread1.Priority = _ ThreadPriority.BelowNormal

L'image a radicalement changé : le deuxième flux a pris presque tout le temps du premier (Fig. 4).

Notez également l'utilisation de la méthode Join. Avec son aide, nous effectuons une variante assez courante de la synchronisation des threads, dans laquelle le programme principal attend l'achèvement de plusieurs processus de calcul parallèles.

Conclusion

Nous venons d'aborder les bases du développement d'applications .NET multithread. L'un des problèmes les plus difficiles et les plus d'actualité en pratique est la synchronisation des threads. En plus d'utiliser l'objet Thread décrit dans cet article (il possède de nombreuses méthodes et propriétés que nous n'avons pas considérées ici), les classes Monitor et Mutex, ainsi que les instructions lock (C#) et SyncLock (VB.NET), jouent un rôle très important dans la gestion des threads. ...

Une description plus détaillée de cette technologie est donnée dans des chapitres séparés des livres et dont je voudrais citer quelques citations (avec lesquelles je suis tout à fait d'accord) comme un très court résumé sur le sujet "Multithreading in .NET".

"Si vous êtes débutant, vous serez peut-être surpris de constater que la surcharge de création et de distribution des threads peut accélérer l'exécution d'une application à thread unique... Essayez donc toujours de tester à la fois des prototypes à thread unique et multi-thread."

"Vous devez faire attention à votre conception multithread et contrôler étroitement l'accès aux objets et variables partagés."

"Ne considérez pas le multithreading comme approche par défaut."

"J'ai demandé à un public de programmeurs VB expérimentés s'ils obtiendraient le threading gratuit dans la future version de VB. Presque tout le monde a levé la main. Ensuite, j'ai demandé qui sait ce qu'il faisait. Cette fois, seules quelques personnes ont levé la main. et il y avait des sourires complices sur leurs visages."

« Si vous n'êtes pas intimidé par les difficultés de conception d'applications multithread, lorsqu'il est utilisé correctement, le multithreading peut considérablement améliorer les performances des applications. »

Pour ma part, j'ajouterais que la technologie de création d'applications .NET multithread (comme de nombreuses autres technologies .NET) dans son ensemble est pratiquement indépendante du langage utilisé. Par conséquent, je conseille aux développeurs d'étudier divers livres et articles, quel que soit le langage de programmation qu'ils choisissent pour démontrer une technologie particulière.

Littérature:

  1. Dan Appleman. Transition vers VB.NET : stratégies, concepts, code / Per. de l'anglais - SPb. : "Pierre", 2002, - 464 p. : ill.
  2. Tom Archer. C# les bases. Les dernières technologies / Par. de l'anglais - M. : Maison d'édition et de commerce « Édition russe », 2001. - 448 p. : ill.

Multitâche et multithreading

Commençons par cette simple déclaration : les systèmes d'exploitation Windows 32 bits prennent en charge les modes de traitement des données multitâche (multitraitement) et multithread. Il est possible de discuter de la façon dont ils le font, mais c'est une autre question.

Le multitâche est un mode de fonctionnement où un ordinateur peut effectuer plusieurs tâches en même temps, en parallèle. Il est clair que si un ordinateur dispose d'un processeur, on parle alors de pseudo-parallélisme, lorsque le système d'exploitation, selon certaines règles, peut basculer rapidement entre différentes tâches. Une tâche est un programme ou une partie d'un programme (application) qui exécute une action logique et est l'unité pour laquelle le système d'exploitation alloue des ressources. Sous une forme quelque peu simplifiée, nous pouvons supposer que dans Windows, une tâche est chaque composant logiciel implémenté en tant que module exécutable distinct (EXE, DLL). Pour Windows, la notion de « tâche » a la même signification que « processus », qui désigne notamment l'exécution de code de programme strictement dans l'espace d'adressage qui lui est alloué.

Il existe deux principaux types de multitâche : coopératif et préemptif. La première option, implémentée dans les versions antérieures de Windows, permet de basculer entre les tâches uniquement au moment où la tâche active accède au système d'exploitation (par exemple, pour les E/S). Dans ce cas, chaque thread est chargé de rendre le contrôle au système d'exploitation. Si la tâche oubliait d'effectuer une telle opération (par exemple, elle restait bloquée dans une boucle), cela entraînait souvent le blocage de l'ensemble de l'ordinateur.

Le multitâche préemptif est un mode dans lequel le système d'exploitation lui-même est chargé de donner à chaque thread sa tranche de temps due, après quoi il (s'il y a des demandes d'autres tâches) interrompt automatiquement ce thread et décide quoi commencer ensuite. Auparavant, ce mode était appelé « temps partagé ».

Qu'est-ce qu'un flux ? Un thread est un processus de calcul autonome, mais non alloué au niveau du système d'exploitation, mais au sein d'une tâche. La différence fondamentale entre un thread et une "tâche de processus" est que tous les threads d'une tâche sont exécutés dans un seul espace d'adressage, c'est-à-dire qu'ils peuvent fonctionner avec des ressources de mémoire partagées. C'est précisément là que résident leurs avantages (traitement parallèle des données) et leurs inconvénients (menace pour la fiabilité du programme). Ici, il convient de garder à l'esprit que dans le cas du multitâche, le système d'exploitation est principalement responsable de la protection des applications et, lors de l'utilisation du multithread, le développeur lui-même.

Notez que l'utilisation du mode multitâche dans les systèmes monoprocesseur permet d'augmenter les performances globales du système multitâche dans son ensemble (mais pas toujours, car à mesure que le nombre de commutateurs augmente, la part des ressources occupées par l'OS augmente). Mais le temps d'exécution d'une tâche spécifique augmente toujours, même légèrement, en raison du travail supplémentaire du système d'exploitation.

Si le processeur est fortement chargé en tâches (avec un temps d'arrêt minimal pour les E/S, par exemple, dans le cas de la résolution de problèmes purement mathématiques), une réelle augmentation des performances globales n'est obtenue qu'en utilisant des systèmes multiprocesseurs. De tels systèmes permettent différents modèles de parallélisation - au niveau de la tâche (chaque tâche ne peut occuper qu'un seul processeur, alors que les threads ne sont exécutés qu'en pseudo-parallélisme) ou au niveau du thread (quand une tâche peut occuper plusieurs processeurs avec ses threads).

Ici, vous pouvez également rappeler que lors de l'exploitation de puissants systèmes informatiques partagés, dont l'ancêtre était la famille IBM System / 360 à la fin des années 60, l'une des tâches les plus urgentes était de choisir l'option de contrôle multitâche optimale - y compris en mode dynamique, en tenant compte de divers paramètres. En principe, le contrôle multitâche est une fonction du système d'exploitation. Mais l'efficacité de la mise en œuvre de l'une ou l'autre option est directement liée aux particularités de l'architecture de l'ordinateur dans son ensemble, et notamment du processeur. Par exemple, le même IBM System / 360 hautes performances fonctionnait bien dans les systèmes partagés pour les tâches commerciales, mais en même temps, il était totalement inadapté à la résolution des problèmes de la classe "temps réel". À cette époque, des mini-ordinateurs nettement moins chers et plus simples tels que le DEC PDP 11/20 étaient clairement en tête dans ce domaine.

Quel sujet soulève le plus de questions et de difficultés pour les débutants ? Lorsque j'ai interrogé mon professeur et programmeur Java Alexander Pryakhin à ce sujet, il a immédiatement répondu : « Multithreading ». Merci à lui pour l'idée et l'aide à la préparation de cet article !

Nous examinerons le monde intérieur de l'application et de ses processus, découvrirons quelle est l'essence du multithreading, quand il est utile et comment l'implémenter - en utilisant Java comme exemple. Si vous apprenez un autre langage POO, ne vous inquiétez pas : les principes de base sont les mêmes.

À propos des flux et de leurs origines

Pour comprendre le multithreading, commençons par comprendre ce qu'est un processus. Un processus est une partie de la mémoire virtuelle et des ressources que le système d'exploitation alloue pour exécuter un programme. Si vous ouvrez plusieurs instances de la même application, le système allouera un processus pour chacune. Dans les navigateurs modernes, un processus distinct peut être responsable de chaque onglet.

Vous avez probablement rencontré le "Gestionnaire des tâches" de Windows (sous Linux, il s'agit du "Moniteur système") et vous savez que les processus en cours d'exécution inutiles chargent le système et que les plus "lourds" d'entre eux se bloquent souvent, ils doivent donc être arrêtés de force .

Mais les utilisateurs adorent le multitâche : ne leur donnez pas de pain - ouvrez simplement une douzaine de fenêtres et sautez d'avant en arrière. Il y a un dilemme : vous devez assurer le fonctionnement simultané des applications et en même temps réduire la charge sur le système afin qu'il ne ralentisse pas. Disons que le matériel ne peut pas répondre aux besoins des propriétaires - vous devez résoudre le problème au niveau logiciel.

Nous voulons que le processeur exécute plus d'instructions et traite plus de données par unité de temps. C'est-à-dire que nous devons insérer davantage de code exécuté dans chaque tranche de temps. Considérez une unité d'exécution de code comme un objet — c'est un thread.

Un cas complexe est plus facile à aborder si vous le décomposez en plusieurs cas simples. Il en est ainsi lorsque l'on travaille avec de la mémoire : un processus "lourd" est divisé en threads qui consomment moins de ressources et sont plus susceptibles de livrer le code à la calculatrice (voir ci-dessous pour savoir comment exactement).

Chaque application a au moins un processus, et chaque processus a au moins un thread, qui est appelé le thread principal et à partir duquel, si nécessaire, de nouveaux sont lancés.

Différence entre les threads et les processus

    Les threads utilisent la mémoire allouée au processus et les processus nécessitent leur propre espace mémoire. Par conséquent, les threads sont créés et terminés plus rapidement : le système n'a pas besoin de leur allouer un nouvel espace d'adressage à chaque fois, puis de le libérer.

    Les processus fonctionnent chacun avec leurs propres données - ils ne peuvent échanger quelque chose que via le mécanisme de communication interprocessus. Les threads accèdent directement aux données et aux ressources des autres : ce que l'on a modifié est immédiatement disponible pour tout le monde. Le fil peut contrôler le « compagnon » dans le processus, tandis que le processus contrôle exclusivement ses « filles ». Par conséquent, la commutation entre les flux est plus rapide et la communication entre eux est plus facile.

Quelle en est la conclusion ? Si vous devez traiter une grande quantité de données aussi rapidement que possible, divisez-les en morceaux qui peuvent être traités par des threads séparés, puis reconstituez le résultat. C'est mieux que d'engendrer des processus gourmands en ressources.

Mais pourquoi une application populaire comme Firefox s'engage-t-elle dans la création de plusieurs processus ? Parce que c'est pour le navigateur que les onglets isolés fonctionnent est fiable et flexible. Si quelque chose ne va pas avec un processus, il n'est pas nécessaire de terminer le programme entier - il est possible de sauvegarder au moins une partie des données.

Qu'est-ce que le multithreading

Nous arrivons donc à l'essentiel. Le multithreading se produit lorsque le processus d'application est divisé en threads qui sont traités en parallèle - à une unité de temps - par le processeur.

La charge de calcul est répartie entre deux cœurs ou plus, de sorte que l'interface et les autres composants du programme ne se ralentissent pas mutuellement.

Les applications multithreads peuvent être exécutées sur des processeurs monocœur, mais les threads sont ensuite exécutés à tour de rôle : le premier a fonctionné, son état a été enregistré - le second a été autorisé à fonctionner, enregistré - est revenu au premier ou a lancé le troisième, etc.

Les gens occupés se plaignent de n'avoir que deux mains. Les processus et les programmes peuvent avoir autant de mains que nécessaire pour terminer la tâche le plus rapidement possible.

Attendre un signal : synchronisation dans les applications multi-thread

Imaginez que plusieurs threads essaient de modifier la même zone de données en même temps. Quelles modifications seront finalement acceptées et quelles modifications seront annulées ? Pour éviter toute confusion avec les ressources partagées, les threads doivent coordonner leurs actions. Pour ce faire, ils échangent des informations à l'aide de signaux. Chaque thread dit aux autres ce qu'il fait et les changements à attendre. Ainsi, les données de tous les threads sur l'état actuel des ressources sont synchronisées.

Outils de synchronisation de base

Exclusion mutuelle (exclusion mutuelle, abrégé en mutex) - un "drapeau" allant au fil qui est actuellement autorisé à travailler avec des ressources partagées. Élimine l'accès par d'autres threads à la zone mémoire occupée. Il peut y avoir plusieurs mutex dans une application, et ils peuvent être partagés entre les processus. Il y a un hic : mutex force l'application à accéder au noyau du système d'exploitation à chaque fois, ce qui est coûteux.

Sémaphore - permet de limiter le nombre de threads pouvant accéder à une ressource à un instant donné. Cela réduira la charge sur le processeur lors de l'exécution de code où il y a des goulots d'étranglement. Le problème est que le nombre optimal de threads dépend de la machine de l'utilisateur.

Événement - vous définissez une condition à l'apparition de laquelle le contrôle est transféré au thread souhaité. Les flux échangent des données d'événement pour développer et poursuivre logiquement les actions de chacun. L'un a reçu les données, l'autre a vérifié leur exactitude, le troisième les a enregistrées sur le disque dur. Les événements diffèrent dans la manière dont ils sont annulés. Si vous devez notifier plusieurs threads d'un événement, vous devrez définir manuellement la fonction d'annulation pour arrêter le signal. S'il n'y a qu'un seul thread cible, vous pouvez créer un événement de réinitialisation automatique. Il arrêtera le signal lui-même après avoir atteint le flux. Les événements peuvent être mis en file d'attente pour un contrôle de flux flexible.

Section critique - un mécanisme plus complexe qui combine un compteur de boucles et un sémaphore. Le compteur permet de différer le démarrage du sémaphore pour le temps souhaité. L'avantage est que le noyau n'est activé que si la section est occupée et que le sémaphore doit être activé. Le reste du temps, le thread s'exécute en mode utilisateur. Hélas, une section ne peut être utilisée que dans un seul processus.

Comment implémenter le multithreading en Java

La classe Thread est responsable du travail avec les threads en Java. Créer un nouveau thread pour exécuter une tâche signifie créer une instance de la classe Thread et l'associer au code souhaité. Ceci peut être fait de deux façons:

    sous-classe Thread;

    implémentez l'interface Runnable dans votre classe, puis transmettez les instances de classe au constructeur Thread.

Bien que nous n'abordions pas le sujet des blocages, lorsque les threads se bloquent mutuellement et se bloquent, nous laisserons cela pour le prochain article.

Exemple de multithreading Java : ping pong avec mutex

Si vous pensez que quelque chose de terrible est sur le point de se produire, expirez. Nous envisagerons de travailler avec des objets de synchronisation de manière presque ludique : deux threads seront lancés par un mutex. Mais en fait, vous verrez une application réelle où un seul thread peut traiter des données publiquement disponibles à la fois.

Tout d'abord, créons une classe qui hérite des propriétés du Thread que nous connaissons déjà, et écrivons une méthode kickBall :

La classe publique PingPongThread étend Thread (PingPongThread (nom de la chaîne) (this.setName (nom); // remplace le nom du thread) @Override public void run () (Ball ball = Ball.getBall (); tandis que (ball.isInGame () ) (kickBall (ball);)) kickBall privé vide (ball ball) (si (! ball.getSide (). est égal à (getName ())) (ball.kick (getName ());)))

Occupons-nous maintenant du ballon. Il ne sera pas simple avec nous, mais mémorable : pour qu'il puisse dire qui l'a frappé, de quel côté et combien de fois. Pour ce faire, nous utilisons un mutex : il collectera des informations sur le travail de chacun des threads - cela permettra aux threads isolés de communiquer entre eux. Après le 15e coup, nous retirerons le ballon du jeu, afin de ne pas le blesser gravement.

Classe publique Ball (coups int privés = 0 ; instance de balle statique privée = nouvelle balle (); côté chaîne privée = ""; balle privée () () balle statique getBall () (instance de retour ;) coup de pied nul synchronisé (nom du joueur de chaîne) (coups de pied ++; côté = nom du joueur; System.out.println (coups de pied + "" + côté);) String getSide () (côté de retour;) booléen isInGame () (retour (coups de pied< 15); } }

Et maintenant, deux threads de joueurs entrent en scène. Appelons-les, sans plus tarder, Ping et Pong :

Classe publique PingPongGame (PingPongThread player1 = nouveau PingPongThread ("Ping"); PingPongThread player2 = nouveau PingPongThread ("Pong"); Ball ball; PingPongGame () (ball = Ball.getBall ();) void startGame () lève InterruptedException (player1 .start (); player2.start ();))

"Un stade plein de monde - il est temps de commencer le match." Nous annoncerons officiellement l'ouverture de la réunion - dans la classe principale de l'application :

Classe publique PingPong (public static void main (String args) lève InterruptedException (PingPongGame game = new PingPongGame (); game.startGame ();))

Comme vous pouvez le voir, il n'y a rien de furieux ici. Ceci n'est qu'une introduction au multithreading pour l'instant, mais vous savez déjà comment cela fonctionne, et vous pouvez expérimenter - limitez la durée du jeu non pas par le nombre de coups, mais par le temps, par exemple. Nous reviendrons plus tard sur le sujet du multithreading - nous examinerons le package java.util.concurrent, la bibliothèque Akka et le mécanisme volatile. Parlons également de l'implémentation du multithreading en Python.

Vous avez aimé l'article ? A partager entre amis :