Existe-t-il des pratiques obsolètes pour la programmation multithreads et multiprocesseurs que je ne devrais plus utiliser?

36

Aux débuts de FORTRAN et de BASIC, pratiquement tous les programmes étaient écrits avec des déclarations GOTO. Le résultat était un code spaghetti et la solution était une programmation structurée.

De même, les pointeurs peuvent avoir des caractéristiques difficiles à contrôler dans nos programmes. C ++ a commencé avec beaucoup de pointeurs, mais l'utilisation de références est recommandée. Des bibliothèques telles que STL peuvent réduire certaines de nos dépendances. Il existe également des idiomes pour créer des pointeurs intelligents présentant de meilleures caractéristiques, et certaines versions de C ++ permettent des références et du code géré.

Les pratiques de programmation telles que l'héritage et le polymorphisme utilisent beaucoup de pointeurs dans les coulisses (comme pour la programmation structurée, le code est rempli d'instructions de branche). Des langages tels que Java éliminent les pointeurs et utilisent la récupération de place pour gérer les données allouées dynamiquement au lieu de dépendre des programmeurs pour qu'ils correspondent à leurs nouvelles instructions et à leurs instructions delete.

Dans ma lecture, j'ai vu des exemples de programmation multi-processus et multi-threads qui ne semblent pas utiliser de sémaphores. Utilisent-ils la même chose avec des noms différents ou disposent-ils de nouvelles méthodes pour structurer la protection des ressources contre les utilisations simultanées?

Par exemple, OpenMP est un exemple spécifique de système de programmation multithread avec des processeurs multicœurs. Il représente une région critique comme suit, sans l'utilisation de sémaphores, qui semblent ne pas être inclus dans l'environnement.

th_id = omp_get_thread_num();
#pragma omp critical
{
  cout << "Hello World from thread " << th_id << '\n';
}

Cet exemple est un extrait de: http://en.wikipedia.org/wiki/OpenMP

Sinon, une protection similaire des threads utilisant des sémaphores avec les fonctions wait () et signal () pourrait ressembler à ceci:

wait(sem);
th_id = get_thread_num();
cout << "Hello World from thread " << th_id << '\n';
signal(sem);

Dans cet exemple, les choses sont assez simples, et une simple revue suffit pour montrer que les appels wait () et signal () sont appariés et même avec beaucoup de simultanéité, la sécurité des threads est assurée. Mais d'autres algorithmes sont plus compliqués et utilisent plusieurs sémaphores (binaires et compteurs) répartis sur plusieurs fonctions avec des conditions complexes pouvant être appelées par de nombreux threads. Il peut être difficile de gérer les conséquences d’une impasse ou de l’absence de sécurité.

Est-ce que ces systèmes comme OpenMP éliminent les problèmes de sémaphores?
Est-ce qu'ils déplacent le problème ailleurs?
Comment transformer mon sémaphore préféré en utilisant un algorithme pour ne plus utiliser de sémaphores?

DeveloperDon
la source
De quoi parlez-vous exactement? Qu'as-tu vu?
svick
4
Cela ne veut pas dire être impoli, mais vous auriez pu éliminer les trois premiers paragraphes. Ils ne traitent pas vraiment de votre question, ils vont trop loin dans leurs conclusions et ne feront que générer beaucoup d'arguments.
Dbracey
1
Whoa, grand montage. J'ai eu un coup de poignard à une réponse. La question continue à se balader à travers GOTO, des pointeurs, des héritages et du polymorphisme, mais dans ma réponse, j’ai mis ces problèmes de côté et je me suis concentrée sur la question des "pratiques déconseillées".
Stuart marque

Réponses:

15

Existe-t-il des techniques et des pratiques de programmation simultanées qu’on ne devrait plus utiliser? Je dirais oui .

Une des premières techniques de programmation simultanée qui semble rare de nos jours est la programmation par interruptions . C’est ainsi que fonctionnait UNIX dans les années 1970. Voir le Commentaire Lions sur UNIX ou la conception du système d'exploitation UNIX par Bach . En bref, la technique consiste à suspendre temporairement les interruptions lors de la manipulation d'une structure de données, puis à restaurer les interruptions par la suite. La page de manuel BSD spl (9)a un exemple de ce style de codage. Notez que les interruptions sont axées sur le matériel et que le code incarne une relation implicite entre le type d'interruption matérielle et les structures de données associées à ce matériel. Par exemple, le code manipulant les tampons d'E / S de disque doit suspendre les interruptions du matériel du contrôleur de disque lorsqu'il travaille avec ces tampons.

Ce style de programmation était utilisé par les systèmes d'exploitation sur du matériel à un seul processeur. Il était beaucoup plus rare que les applications traitent des interruptions. Certains systèmes d'exploitation avaient des interruptions logicielles, et je pense que les gens ont essayé de construire des systèmes de threading ou de coroutine dessus, mais cela n'était pas très répandu. (Certainement pas dans le monde UNIX.) Je soupçonne que la programmation de type interruption est aujourd'hui limitée aux petits systèmes embarqués ou aux systèmes temps réel.

Les sémaphores constituent un progrès par rapport aux interruptions car ce sont des constructions logicielles (non liées au matériel), ils fournissent des abstractions sur des installations matérielles et permettent le multithreading et le multitraitement. Le problème principal est qu’ils ne sont pas structurés. Le programmeur est responsable du maintien de la relation entre chaque sémaphore et les structures de données qu'il protège, de manière globale pour l'ensemble du programme. Pour cette raison, je pense que les sémaphores nus sont rarement utilisés aujourd'hui.

Un autre petit pas en avant est un moniteur , qui encapsule les mécanismes de contrôle de la concurrence (verrous et conditions) avec les données protégées. Cela a été reporté dans le système Mesa (lien alternatif) et de là dans Java. (Si vous lisez ce document Mesa, vous verrez que les verrous et les conditions du moniteur Java sont copiés presque intégralement de Mesa.) Les moniteurs sont utiles car un programmeur suffisamment prudent et diligent peut écrire des programmes concurrents en toute sécurité en utilisant uniquement un raisonnement local sur le code et les données. dans le moniteur.

Il existe d'autres structures de bibliothèque, telles que celles du java.util.concurrentpackage Java , qui incluent une variété de structures de données hautement concurrentes et de structures de regroupement de threads. Celles-ci peuvent être combinées à des techniques supplémentaires telles que le confinement du fil et l'immutabilité effective. Voir Java Concurrency In Practice de Goetz et. Al. pour plus de discussion. Malheureusement, de nombreux programmeurs continuent de lancer leurs propres structures de données avec des verrous et des conditions, alors qu'ils devraient vraiment utiliser quelque chose comme ConcurrentHashMap où les auteurs de la bibliothèque ont déjà fait le gros du travail.

Tout ce qui précède partage certaines caractéristiques significatives: ils ont plusieurs threads de contrôle qui interagissent sur un état mutable et partagé dans le monde entier . Le problème est que la programmation dans ce style est toujours très sujette aux erreurs. Il est assez facile pour une petite erreur de passer inaperçue, ce qui entraîne une mauvaise conduite difficile à reproduire et à diagnostiquer. Il se peut qu'aucun programmeur ne soit "suffisamment prudent et diligent" pour développer de grands systèmes de cette manière. Au moins, très peu le sont. Donc, je dirais que la programmation multi-thread avec un état mutable et mutable devrait être évitée dans la mesure du possible.

Malheureusement, on ne sait pas très bien si cela peut être évité dans tous les cas. Beaucoup de programmation est encore faite de cette façon. Ce serait bien de voir cela supplanté par autre chose. Les réponses de Jarrod Roberson et davidk01 indiquent des techniques telles que les données immuables, la programmation fonctionnelle, le STM et la transmission de messages. Il y a beaucoup à leur recommander, et tous sont activement développés. Mais je ne pense pas qu'ils aient complètement remplacé le bon état mutable à l'ancienne et partagé pour l'instant.

EDIT: voici ma réponse aux questions spécifiques à la fin.

Je ne connais pas grand chose à propos d'OpenMP. Mon impression est que cela peut être très efficace pour des problèmes très parallèles tels que des simulations numériques. Mais cela ne semble pas être d'usage général. Les constructions de sémaphore semblent de bas niveau et obligent le programmeur à maintenir la relation entre les sémaphores et les structures de données partagées, avec tous les problèmes que j'ai décrits ci-dessus.

Si vous avez un algorithme parallèle qui utilise des sémaphores, je ne connais aucune technique générale pour le transformer. Vous pourrez peut-être le transformer en objets, puis construire des abstractions autour de celui-ci. Mais si vous voulez utiliser quelque chose comme la transmission de messages, je pense que vous devez vraiment reconceptualiser l’ensemble du problème.

Stuart Marks
la source
Merci, c'est une excellente information. Je vais parcourir les références et approfondir les concepts que vous avez mentionnés et qui sont nouveaux pour moi.
DeveloperDon
+1 pour java.util.concurrent et a accepté le commentaire - il est dans le JDK depuis la version 1.5 et je le vois rarement, voire jamais, utilisé.
MebAlone
1
Je souhaite que vous souligniez combien il est important de ne pas créer vos propres structures quand celles-ci existent déjà. Tant de bugs ...
corsiKa
Je ne pense pas qu'il soit juste de dire: "Les sémaphores sont un progrès par rapport aux interruptions, car ils sont des constructions logicielles (non liées au matériel) ". Les sémaphores dépendent de la CPU pour implémenter l' instruction Compare-and-Swap ou de ses variantes multicœurs .
Josh Pearce
@JoshPearce Bien sûr, les sémaphores sont implémentés à l' aide de constructions matérielles, mais il s'agit d'une abstraction indépendante de toute construction matérielle particulière, telle que CAS, test-and-set, cmpxchng, etc.
Stuart Marks
28

Réponse à la question

Le consensus général est que l' état mutable partagé est Bad ™ et que l'état immuable est Good ™, ce qui prouve son exactitude et sa véracité encore et encore par les langages fonctionnels et les langages impératifs.

Le problème, c’est que les langues impératives ordinaires ne sont tout simplement pas conçues pour gérer cette façon de travailler, les choses ne vont pas changer pour ces langues du jour au lendemain. C'est là que la comparaison GOTOest imparfaite. L'état immuable et la transmission de messages sont une excellente solution, mais ce n'est pas non plus une panacée.

Locaux imparfaits

Cette question est basée sur des comparaisons avec une prémisse erronée; qui GOTOétait le problème réel et a été universellement dépréciée d' une façon par les syndicats Intergalatic Conseil universelle des langues concepteurs et génie logiciel ©! Sans GOTOmécanisme, l'ASM ne fonctionnerait pas du tout. Idem avec le principe que les pointeurs bruts sont le problème avec C ou C ++ et que certains pointeurs intelligents sont une panacée, ils ne le sont pas.

GOTOCe n'était pas le problème, ce sont les programmeurs. Même chose pour l' état mutable partagé . En soi, ce n'est pas le problème , ce sont les programmeurs qui l'utilisent qui pose problème. S'il existait un moyen de générer du code utilisant un état mutable partagé de manière à ce qu'il n'y ait jamais eu de problème de concurrence ou de bogue, cela ne poserait pas de problème. C'est un peu comme si vous GOTOn'écriviez jamais de code spaghetti avec ou des constructions équivalentes, ce n'était pas un problème non plus.

L'éducation est la panacée

Les programmeurs sont idiotes ce sont deprecated, chaque langue populaire a toujours la GOTOconstruction soit directement ou indirectement , et il est un best practicesi correctement utilisé dans toutes les langues qui a ce type de constructions.

Exemple: Java a des étiquettes et les try/catch/finallydeux qui travaillent directement comme des GOTOdéclarations.

La plupart des programmeurs Java à qui je parle ne savent même pas ce que cela immutablesignifie réellement en dehors d'eux répétant the String class is immutableavec un zombie comme le regard dans les yeux. Ils ne savent vraiment pas comment utiliser le finalmot clé correctement pour créer une immutableclasse. Je suis donc presque sûr qu'ils ne savent pas pourquoi la transmission de messages à l' aide de messages immuables est si géniale et pourquoi l'état mutable partagé n'est pas si génial.

Communauté
la source
3
+1 Grande réponse, clairement écrite et indiquant le motif sous-jacent d'état mutable. IUBLDSEU devrait devenir un meme :)
Dibbeke
2
GOTO est un mot de code pour 's'il vous plaît, non vraiment s'il vous plaît commencer une guerre de flammes ici, je double chien vous osez ». Cette question éteint les flammes mais ne donne pas vraiment une bonne réponse. Les mentions honorables de programmation fonctionnelle et d'immutabilité sont excellentes, mais ces affirmations sont sans fondement.
Evan Plaice
1
Cela semble être une réponse contradictoire. Tout d'abord, vous dites "A est mauvais, B est bon", puis vous dites "Les idiots ont été déconseillés". La même chose ne s'applique-t-elle pas au premier paragraphe? Ne puis-je pas simplement prendre la dernière partie de votre réponse et dire «L’état mutable partagé est une pratique recommandée lorsqu’il est correctement utilisé dans toutes les langues». En outre, "preuve" est un mot très fort. Vous ne devriez pas l' utiliser sauf si vous avez vraiment des preuves solides.
luiscubal
2
Ce n'était pas mon intention de déclencher une guerre de flammes. Jusqu'à ce que Jarrod réagisse à mon commentaire, il avait pensé que GOTO n'était pas controversé et fonctionnerait bien dans une analogie. Lorsque j'ai écrit la question, cela ne m'est pas venu à l'esprit, mais Dijkstra était à zéro sur les deux GOTO et les sémaphores. Edsger Dijkstra me semble être un géant. On lui attribue l'invention des sémaphores (1965) et du début (1968) des travaux scientifiques sur les GOTO. La méthode de plaidoyer de Dijkstra était souvent croustillante et conflictuelle. La controverse / la confrontation a fonctionné pour lui, mais je veux juste des idées sur les alternatives possibles aux sémaphores.
DeveloperDon
1
De nombreux programmes sont supposés modéliser des choses qui, dans le monde réel, sont mutables. Si à 5h37, l'objet n ° 451 maintient l'état de quelque chose dans le monde réel à ce moment-là (5:37), et que l'état de la chose du monde réel change par la suite, il est possible que l'identité de l'objet représentant l'état de la chose du monde réel doit être immuable (c'est-à-dire que la chose sera toujours représentée par l'objet # 451), ou que l'objet # 451 soit immuable, mais pas les deux. Dans de nombreux cas, avoir l'identité soit immuable sera plus utile que l'objet # 451 soit immuable.
Supercat
27

La dernière en date dans les milieux universitaires semble être la mémoire logicielle transactionnelle (STM), qui promet de supprimer tous les détails complexes de la programmation multithread des programmeurs en utilisant une technologie de compilateur suffisamment intelligente. En coulisse, il y a toujours des verrous et des sémaphores, mais vous en tant que programmeur n'avez pas à vous en préoccuper. Les avantages de cette approche ne sont toujours pas clairs et il n'y a pas de candidats évidents.

Erlang utilise des agents de transmission de messages et des agents pour l'accès simultané. Il s'agit d'un modèle plus simple à utiliser que STM. Avec la transmission de messages, vous n'avez absolument aucun souci à vous poser sur les verrous et les sémaphores, car chaque agent fonctionne dans son propre mini-univers. Il n'y a donc pas de conditions de concurrence liées aux données. Vous avez encore quelques cas étranges, mais ils sont loin d'être aussi compliqués que les vivres et les impasses. Les langues de la machine virtuelle Java peuvent utiliser Akka et bénéficier de tous les avantages de la transmission de messages et d'acteurs. Contrairement à Erlang, la machine virtuelle Java ne prend pas en charge les acteurs. A la fin de la journée, Akka utilise toujours des threads et des verrous le programmeur n'a pas à s'en soucier.

L’autre modèle que je connais et qui n’utilise pas de verrous et de threads est l’utilisation de futures, qui est en réalité une autre forme de programmation async.

Je ne sais pas dans quelle mesure cette technologie est disponible en C ++, mais il est probable que si vous voyez quelque chose qui n'utilise pas explicitement les threads et les verrous, il s'agira d'une des techniques ci-dessus pour gérer les accès simultanés.

davidk01
la source
+1 pour le nouveau terme "détails poilus". LOL man. Je ne peux pas m'empêcher de rire de ce nouveau mandat. Je suppose que je vais utiliser le "code poilu" à partir de maintenant.
Saeed Neamati
1
@ Saeed: J'ai déjà entendu cette expression, ce n'est pas si rare. Je suis d'accord que c'est drôle cependant :-)
Cameron
1
Bonne réponse. La CLI .NET est censée également prendre en charge la signalisation (par opposition au verrouillage), mais je n'ai pas encore trouvé d'exemple où elle a complètement remplacé le verrouillage. Je ne sais pas si async compte. Si vous parlez de plates-formes comme Javascript / NodeJs, elles sont en réalité mono-threadées et ne sont meilleures que pour des charges d'IO élevées, car elles sont beaucoup moins susceptibles de limiter les ressources au maximum (par exemple, sur une tonne de contextes à jeter). L'utilisation de la programmation async présente peu ou pas d'avantages sur les charges gourmandes en ressources CPU.
Evan Plaice
1
Réponse intéressante, je n'avais jamais rencontré d' avenir . Notez également que vous pouvez toujours avoir une impasse et un livelock dans des systèmes de transmission de messages comme Erlang . CSP vous permet de raisonner officiellement sur l' impasse et sur livelock, mais cela ne l'empêche pas par lui-même.
Mark Booth
1
J'ajouterais à cette liste les structures de données Lock Free et Wait Free.
Stonemetal
3

Je pense que cela concerne principalement les niveaux d'abstraction. Très souvent, en programmation, il est utile d’abréger certains détails de manière plus sûre ou plus lisible ou quelque chose du genre.

Ceci s'applique aux structures de contrôle: ifs, fors et pairs try- les catchblocs ne sont que des abstractions de gotos. Ces abstractions sont presque toujours utiles, car elles rendent votre code plus lisible. Mais il y a des cas où vous aurez toujours besoin d'utiliser goto(par exemple, si vous écrivez l'assemblage à la main).

Cela s'applique également à la gestion de la mémoire: les pointeurs intelligents C ++ et GC sont des abstractions sur des pointeurs bruts et une dés / allocation de mémoire manuelle. Et parfois, ces abstractions ne sont pas appropriées, par exemple lorsque vous avez vraiment besoin de performances maximales.

Et la même chose s'applique au multi-threading: des choses comme les futurs et les acteurs ne sont que des abstractions sur des threads, des sémaphores, des mutexes et des instructions CAS. De telles abstractions peuvent vous aider à rendre votre code beaucoup plus lisible et à éviter les erreurs. Mais parfois, ils ne sont tout simplement pas appropriés.

Vous devriez savoir quels outils vous avez à votre disposition et quels sont leurs avantages et leurs inconvénients. Ensuite, vous pouvez choisir l'abstraction correcte pour votre tâche (le cas échéant). Des niveaux d'abstraction plus élevés ne déprécient pas les niveaux plus bas, il y aura toujours des cas où l'abstraction n'est pas appropriée et le meilleur choix consiste à utiliser la méthode «ancienne».

svick
la source
Merci, vous attrapez l'analogie, et je n'ai pas d'idée préconçue ni même de savoir si la réponse des sémaphores WRT est qu'elles sont ou non obsolètes. La plus grande question qui se pose à moi est de savoir s'il existe de meilleurs moyens et que, dans les systèmes où il ne semble pas y avoir de sémaphores, il manque quelque chose d'important et qui seraient incapables de faire toute la gamme des algorithmes multithreads.
DeveloperDon
2

Oui, mais vous ne rencontrerez probablement pas certains d'entre eux.

À l’époque, il était courant d’utiliser des méthodes de blocage (synchronisation de barrière) car il était difficile d’écrire de bons mutex. Vous pouvez toujours voir des traces de cela dans les choses récentes. L'utilisation de bibliothèques de concurrence simultanées vous donne un ensemble d'outils beaucoup plus riches et testés pour la parallélisation et la coordination interprocessus.

De même, une pratique plus ancienne consistait à écrire du code tortueux de manière à ce que vous puissiez trouver un moyen de le mettre en parallèle manuellement. Cette forme d’optimisation (potentiellement nuisible, si vous vous trompez) a également largement disparu avec l’avènement des compilateurs qui le font pour vous, défilant les boucles si nécessaire, suivant les branches de manière prédictive, etc. Ce n’est cependant pas une nouvelle technologie. , étant au moins 15 ans sur le marché. Profiter de choses comme les pools de threads permet également de contourner certains codes très astucieux d’antan.

Donc, la pratique obsolète est peut-être d’écrire vous-même le code de concurrence, au lieu d’utiliser des bibliothèques modernes et bien testées.

Alex Feinman
la source
Merci. Il semble que le potentiel d’utilisation de la programmation concurrente soit prometteur, mais ce pourrait être une boîte de Pandore s’il n’est pas utilisé de manière disciplinée.
DeveloperDon
2

Grand Central Dispatch d'Apple est une abstraction élégante qui a changé ma vision de la concurrence. L’accent mis sur les files d’attente facilite la mise en oeuvre de la logique asynchrone, selon mon humble expérience.

Lorsque je programme dans des environnements où cela est disponible, cela a remplacé la plupart de mes utilisations de threads, de verrous et de communication inter-thread.

orip
la source
1

L'un des principaux changements apportés à la programmation parallèle réside dans le fait que les processeurs sont considérablement plus rapides qu'auparavant, mais pour obtenir ces performances, un cache bien rempli est nécessaire. Si vous essayez d'exécuter plusieurs threads en même temps en les échangeant continuellement, vous allez presque toujours invalider le cache pour chaque thread (c'est-à-dire que chaque thread nécessite des données différentes pour fonctionner) et vous finissez par nuire beaucoup plus aux performances que vous le souhaitez. utilisé avec des processeurs plus lents.

C’est une des raisons pour lesquelles les frameworks asynchrones ou basés sur des tâches (par exemple Grand Central Dispatch ou Intel TBB) sont plus populaires, ils exécutent le code 1 tâche à la fois, en l’achevant avant de passer à la suivante. Cependant, vous devez coder chaque tâche. chaque tâche prend peu de temps à moins que vous ne vouliez visser la conception (c’est-à-dire que vos tâches parallèles sont vraiment mises en file d’attente). Les tâches gourmandes en ressources processeur sont transmises à un autre cœur de processeur au lieu d'être traitées sur un seul thread traitant toutes les tâches. Il est également plus facile à gérer s’il n’ya pas de traitement réellement multithread.

gbjbaanb
la source
Cool, merci pour les références à Apple et Intel Tech. Votre réponse indique-t-elle les défis de la gestion du fil à l'affinité principale? Certains problèmes de performances du cache sont atténués car les processeurs multicœurs peuvent répéter les caches L1 par cœur. Par exemple: software.intel.com/en-us/articles/… Le cache haute vitesse pour quatre cœurs avec plus de résultats de cache peut être plus de 4 fois plus rapide qu'un noyau avec plus de cache manquants sur les mêmes données. La multiplication de matrice peut. La planification aléatoire de 32 threads sur 4 cœurs ne peut pas. Utilisons l'affinité et obtenons 32 cœurs.
DeveloperDon
pas vraiment si c'est le même problème - affinité de base se réfère simplement au problème où une tâche est renvoyée d'un noyau à l'autre. C'est le même problème si une tâche est interrompue, remplacée par une nouvelle, la tâche d'origine continue sur le même noyau. Intel y dit: succès de cache = rapide, erreurs de cache = lent, quel que soit le nombre de cœurs. Je pense qu'ils essaient de vous persuader d'acheter leurs jetons plutôt que des AMD :)
gbjbaanb