Comment expliquer pourquoi le multi-threading est difficile


84

Je suis un assez bon programmeur, mon patron est également un assez bon programmeur. Bien qu'il semble sous-estimer certaines tâches telles que le multi-threading et à quel point cela peut être difficile (je trouve cela très difficile pour autre chose que de lancer quelques threads, d'attendre que tout soit terminé, puis de renvoyer les résultats).

Au moment où vous commencez à vous inquiéter des blocages et des conditions de course, je trouve cela très difficile, mais le patron ne semble pas l'apprécier - je ne pense pas qu'il l'ait jamais vue. Il suffit de taper un verrou sur c'est assez l'attitude.

Alors, comment puis-je le présenter ou expliquer pourquoi il pourrait sous-estimer les complexités de la concurrence, du parallélisme et du multi-threading? Ou peut-être que je me trompe?

Edit: Juste un peu sur ce qu’il a fait - parcourez une liste, pour chaque élément de cette liste, créez un thread qui exécute une commande de mise à jour de la base de données en fonction des informations contenues dans cet élément. Je ne sais pas comment il a contrôlé le nombre de threads exécutés en même temps, je suppose qu'il les a probablement ajoutés à une file d'attente s'il y en avait trop en cours d'exécution (il n'aurait pas utilisé de sémaphore).


17
Le multi-threading est facile. Corriger la synchronisation est difficile.
Vineet Reynolds

33
Amenez trois personnes dans la salle, de préférence avec des accents différents, et demandez-leur d'expliquer en même temps différentes parties qui se chevauchent du problème de la concurrence.
Greyfade

Le multithreading peut être très difficile ou très facile, en fonction du problème et du support linguistique. Clojure, c'est facile clojure.org/concurrent_programming
Emploi

4
@Job La programmation simultanée est toujours difficile (dans des projets réels), peu importe la langue utilisée. Scala, Clojure ou Erlang le rendent un peu sain d’esprit lorsque vous voulez le comparer à des langages qui utilisent et encouragent les états mutables.
Chiron

4
Ma métaphore préférée pour cela est: "Voulez-vous prendre un somnifère et un laxatif en même temps?" Même en utilisant des files de messages complexes, l'ordre est le fruit d'une concurrence bien faite . Que, à moins que vous avez une grande quantité d'expérience avec elle, est difficile pour beaucoup de gens.
Tim Post

Réponses:


29
  1. Si vous pouvez compter sur une expérience mathématique quelconque, montrez comment un flux d’exécution normal, essentiellement déterministe, devient non seulement non déterministe avec plusieurs threads, mais de manière exponentielle , car vous devez vous assurer que tout entrelacement possible d’instructions machine fera tout de même le bon choix. Un exemple simple de mise à jour perdue ou de situation de lecture incorrecte est souvent révélateur.

  2. "Giflez un verrou" est la solution triviale ... elle résout tous vos problèmes si vous n'êtes pas préoccupé par la performance. Essayez d’illustrer l’impact sur les performances si, par exemple, Amazon devait verrouiller toute la côte est chaque fois qu’un citoyen d’Atlanta commande un livre!


1
+1 pour la discussion sur la complexité mathématique - c’est ainsi que j’ai compris la difficulté de la concurrence entre états partagés, et c’est l’argument que j’ai le plus souvent avancé pour défendre les architectures de transmission de messages. -1 pour "gifler un verrou" ... Cette phrase évoque une approche irréfléchie de l'utilisation de verrous, qui mènera très probablement à une impasse ou à un comportement incohérent demandes, mais ne se synchronisent pas entre eux, les clients auront des modèles incompatibles de l’état de votre bibliothèque).
Aidan Cully

2
Amazon doit verrouiller brièvement l'inventaire d'un article individuel dans un entrepôt lors du traitement d'une commande. S'il y a un coup énorme et soudain sur un article en particulier, les performances de commande de cet article en souffriront jusqu'à ce que le stock soit épuisé et que l'accès à l'inventaire devienne en lecture seule (et donc à 100% partageable). Amazon a le choix entre d’autres programmes, c’est la capacité de faire la file d’attente des commandes jusqu’à ce qu’un réapprovisionnement ait lieu et la possibilité de gérer les commandes en attente avant qu’un réapprovisionnement soit disponible pour les nouvelles commandes.
Blrfl

@Blrfl: Les programmes peuvent faire cela s'ils sont écrits pour utiliser le message passant par les files d'attente. Il n'est pas nécessaire que tous les messages envoyés à un fil particulier passent par une seule file d'attente…
Donal Fellows

4
@Fellows @Donal: S'il y a 1 million de widgets en stock dans un entrepôt et que 1 million de commandes arrivent au même moment, toutes ces demandes sont sérialisées à un certain niveau tout en faisant correspondre les articles aux commandes, quelle que soit la manière dont elles sont traitées. La réalité pratique est que Amazon n'a probablement jamais tellement de widgets en stock que la latence dans le traitement d'un écrasement de commandes atteigne un niveau inacceptable avant que le stock ne soit épuisé et que tous les autres en ligne puissent être informés (en parallèle): "nous sommes en rupture de stock. " Les files de messages sont un excellent moyen de prévenir les blocages, mais elles ne résolvent pas le problème de la contention élevée pour une ressource limitée.
Blrfl

79

Le multi-threading est simple. Coder une application pour le multi-threading est très, très facile.

Il existe une astuce simple: utiliser une file d'attente de messages bien conçue (ne lancez pas la vôtre) pour transmettre les données entre les threads.

La partie difficile consiste à faire en sorte que plusieurs threads mettent à jour par magie un objet partagé d'une manière ou d'une autre. C'est à ce moment-là que cela devient sujet aux erreurs parce que les gens ne font pas attention aux conditions de course qui sont présentes.

Beaucoup de gens n'utilisent pas de files de messages et tentent de mettre à jour des objets partagés et de se créer des problèmes.

Ce qui devient difficile, c’est de concevoir un algorithme qui fonctionne bien lors de la transmission de données entre plusieurs files d’attente. C'est difficile. Mais la mécanique des threads co-existants (via des files d'attente partagées) est simple.

Notez également que les threads partagent des ressources d'E / S. Un programme lié aux E / S (par exemple, connexions réseau, opérations de fichier ou opérations de base de données) ne va probablement pas aller plus vite avec beaucoup de threads.

Si vous souhaitez illustrer le problème de mise à jour d'objet partagé, c'est simple. Asseyez-vous à travers la table avec un tas de cartes en papier. Écrivez une série de calculs simples - 4 ou 6 formules simples - avec beaucoup d’espace en bas de la page.

Voici le jeu. Chacun lit une formule, écrit une réponse et pose une carte avec la réponse.

Chacun de vous fera la moitié du travail, non? Vous avez terminé dans la moitié du temps, non?

Si votre patron ne pense pas beaucoup et commence tout juste, vous allez vous retrouver en conflit d'une manière ou d'une autre et écrire les réponses à la même formule. Cela n'a pas fonctionné parce qu'il y a une condition raciale inhérente entre vous lire avant d'écrire. Rien ne vous empêche de lire la même formule et d'écraser les réponses de chacun.

Il existe de très nombreuses manières de créer des conditions de concurrence critique avec des ressources mal verrouillées ou non verrouillées.

Si vous voulez éviter tous les conflits, vous coupez le papier en une pile de formules. Vous en retirez un de la file d'attente, notez la réponse et affichez les réponses. Aucun conflit, car vous lisez tous les deux à partir d'une file de messages à un seul lecteur.


Même couper le papier en une pile ne résout pas totalement le problème - vous vous retrouvez toujours dans une situation où vous et votre patron cherchez une nouvelle formule en même temps et que vous frappez fort avec la sienne. En fait, je dirais que ceci est représentatif du type de problème de threading le plus courant. Les erreurs vraiment grossières sont trouvées tôt. Les erreurs vraiment inhabituelles traînent à jamais parce que personne ne peut les reproduire. Les conditions de course plausibles - comme celle-ci - continuent de se multiplier au cours des tests et finalement, toutes (ou plus probablement la plupart) sont réglées.
Airsource Ltd

@AirsourceLtd Que dites-vous exactement en "frappez vos doigts dans les siens"? Tant que vous avez une file d'attente de messages empêchant deux threads différents de prendre le même message, cela ne pose pas de problème. Sauf si je comprends mal ce que vous vouliez dire.
Zack

25

La programmation multithread est probablement la solution la plus difficile à la concurrence. C'est en gros une abstraction assez basse de ce que fait réellement la machine.

Un certain nombre d'approches, telles que le modèle d'acteur ou la mémoire transactionnelle (logicielle) , sont beaucoup plus simples. Ou travailler avec des structures de données immuables (telles que des listes et des arbres).

Généralement, une séparation appropriée des problèmes facilite le multi-threading. Quelque chose, qui est trop souvent oublié, lorsque les utilisateurs génèrent 20 threads, tous essayant de traiter le même tampon. Utilisez des réacteurs pour lesquels vous avez besoin de synchronisation et transmettez généralement des données entre différents travailleurs dotés de files de messages.
Si vous avez un verrou dans la logique de votre application, vous avez commis une erreur.

Donc oui, techniquement, le multi-threading est difficile.
"Gifler un verrou" est à peu près la solution la moins évolutive aux problèmes de simultanéité, et va à l'encontre de l'objectif même du multi-threading. Cela revient à rétablir un problème dans un modèle d'exécution non simultané. Plus vous le faites, plus il est probable qu’un seul thread soit exécuté à la fois (ou 0 dans une impasse). Il défait le but entier.
C'est comme si on disait "Il est facile de résoudre les problèmes du 3ème monde. Il suffit de lancer une bombe dessus". Ce n’est pas parce qu’il existe une solution triviale que le problème est trivial, car la qualité du résultat est importante.

Mais dans la pratique, la résolution de ces problèmes est aussi difficile que d’autres problèmes de programmation et se fait mieux avec des abstractions appropriées. Ce qui le rend assez facile en fait.


14

Je pense qu'il y a un angle non technique à cette question - l'OMI est une question de confiance. On nous demande généralement de reproduire des applications complexes telles que - oh, je ne sais pas - Facebook par exemple. Je suis parvenu à la conclusion que si vous devez expliquer la complexité d'une tâche à un non-initié / à la direction, alors quelque chose est pourri au Danemark.

Même si d’autres programmeurs ninja peuvent s’acquitter de cette tâche en 5 minutes, vos estimations sont basées sur vos capacités personnelles. Votre interlocuteur devrait soit apprendre à faire confiance à votre opinion sur la question, soit embaucher quelqu'un dont le mot sera prêt à accepter.

Le défi ne consiste pas à relayer les implications techniques que les gens ont tendance à ignorer ou sont incapables de saisir par la conversation, mais à établir une relation de respect mutuel.


1
Réponse intéressante, bien que ce soit une question technique. Je suis cependant d'accord avec ce que vous dites ... Dans ce cas, mon responsable est un très bon programmeur. Cependant, comme il n'a pas rencontré la complexité des applications multithreads, il les sous-estime.
M. Shoubs

6

Une simple expérience de pensée pour comprendre les impasses est le problème du " philosophe à manger ". La situation de Therac 25 est l’un des exemples que j’ai tendance à utiliser pour décrire les mauvaises conditions de course .

"Il suffit de taper dessus" est la mentalité de quelqu'un qui n'a pas rencontré de bogues difficiles avec le multi-threading. Et il est possible qu'il pense que vous exagérez la gravité de la situation (je ne le sais pas - il est possible de faire exploser des choses ou de tuer des gens avec des bugs de conditions de compétition, en particulier avec des logiciels embarqués qui aboutissent dans des voitures).


1
C'est-à-dire le problème des sandwichs: vous faites un tas de sandwichs, mais il n'y a qu'un beurre et un couteau. Généralement, tout va bien, mais finalement, quelqu'un s'empare du beurre pendant que quelqu'un d'autre s'empare du couteau… et ensuite, ils restent tous les deux là à attendre que l'autre lâche leurs ressources.
gbjbaanb

Des problèmes de blocage tels que celui-ci pourraient-ils être résolus en acquérant toujours les ressources dans un ordre déterminé?
compman

@compman, non. Parce qu'il est possible pour 2 threads d'essayer de récupérer la même ressource au même moment et que ces threads n'ont pas nécessairement besoin du même ensemble de ressources, il suffit d'un chevauchement suffisant pour causer des problèmes. Une méthode consiste à "remettre" la ressource, puis à attendre une période aléatoire avant de la récupérer. Cette période d'interruption se produit dans un certain nombre de protocoles, dont le plus ancien était Aloha. fr.wikipedia.org/wiki/ALOHAnet
Tangurena

1
Et si chaque ressource du programme avait un numéro, et lorsqu'un thread / processus avait besoin d'un ensemble de ressources, il bloquait toujours les ressources dans un ordre numérique croissant? Je ne pense pas que cette impasse pourrait arriver.
compman

1
@ Compman: C'est en effet un moyen d'éviter l'impasse. Il est possible de concevoir des outils qui vous permettent de vérifier automatiquement cela; donc si votre application ne se trouve pour verrouiller les ressources autres que dans un ordre numérique croissant , alors vous jamais eu un potentiel impasse. (Notez que les blocages potentiels ne se transforment jamais en véritables blocages que lorsque votre code est exécuté sur l'ordinateur d'un client).
gnasher729

3

Les applications concurrentes ne sont pas déterministes. Avec le peu de code global exceptionnellement faible reconnu par le programmeur comme vulnérable, vous ne contrôlez pas le moment où une partie d'un thread / processus s'exécute par rapport à une partie d'un autre thread. Le test est plus difficile, prend plus de temps et il est peu probable que tous les défauts liés à la simultanéité soient détectés. Les défauts, s’ils sont trouvés, sont souvent subtils et ne peuvent pas être reproduits de manière cohérente, ce qui rend leur réparation difficile.

Par conséquent, la seule application concurrente correcte est celle qui est prouvée, quelque chose de peu pratiqué dans le développement de logiciels. En conséquence, la réponse de S.Lot constitue le meilleur conseil général, car il est relativement facile de prouver que le passage du message est correct.


3

Réponse courte en deux mots: OBSERVABLE NONDETERMINISM

Réponse longue: Cela dépend de l'approche que vous utilisez en matière de programmation simultanée, en fonction de votre problème. Dans le livre Concepts, techniques et modèles de programmation informatique , les auteurs expliquent clairement quatre approches pratiques principales pour écrire des programmes simultanés:

  • Programmation séquentielle : une approche de base sans concurrence;
  • Concurrence déclarative : utilisable en l’absence de non-déterminisme observable;
  • Concurrence de passage de message: message simultané passant de nombreuses entités, chaque entité traitant en interne le message de manière séquentielle;
  • Accès simultané aux états partagés : thread mettant à jour des objets passifs partagés au moyen d’actions atomiques à grains grossiers, par exemple des verrous, des moniteurs et des transactions;

Maintenant, la plus simple de ces quatre approches en dehors de la programmation séquentielle évidente est la concurrence simultanée déclarative , car les programmes écrits en utilisant cette approche n’ont pas de non-déterminisme observable . En d'autres termes, il n'y a pas de situation de concurrence , car elle est simplement un comportement non déterministe observable.

Mais le manque de non-déterminisme observable signifie qu'il existe certains problèmes auxquels nous ne pouvons pas nous attaquer à l'aide de la simultanéité déclarative. Voici où les deux dernières approches pas si faciles entrent en jeu. La partie pas si facile est une conséquence du non déterminisme observable. Maintenant, ils tombent tous les deux dans un modèle concomitant à états et ont également une expressivité équivalente. Mais en raison du nombre toujours croissant de cœurs par processeur, il semble que l'industrie se soit davantage intéressée récemment à la concurrence d'accès aux messages, comme en témoigne l'essor des bibliothèques de transmission de messages (par exemple Akka pour JVM) ou des langages de programmation (par exemple Erlang ). .

La bibliothèque Akka mentionnée précédemment, qui s'appuie sur un modèle théorique Actor, simplifie la construction d'applications simultanées, car vous n'avez plus à vous soucier des verrous, des écrans ou des transactions. D'un autre côté, cela nécessite une approche différente de la conception de la solution, c'est-à-dire une manière de hiérarchiser les acteurs composites. On pourrait dire que cela nécessite un état d'esprit totalement différent, ce qui peut encore être finalement plus difficile que d'utiliser une concurrence simultanée partagée.

La programmation concurrente est difficile en raison du non-déterminisme observable, mais lorsque vous utilisez la bonne approche pour le problème donné et la bonne bibliothèque qui prend en charge cette approche, beaucoup de problèmes peuvent être évités.


0

On m'a d'abord appris qu'il pouvait poser problème en voyant un programme simple qui démarrait deux threads et les faisait imprimer tous les deux sur la console en même temps de 1 à 100. Au lieu de:

1
1
2
2
3
3
...

Vous obtenez quelque chose de plus comme ça:

1
2
1
3
2
3
...

Exécutez-le à nouveau et vous obtiendrez des résultats totalement différents.

La plupart d'entre nous ont été formés pour supposer que notre code sera exécuté de manière séquentielle. Avec la plupart des multi-threads, nous ne pouvons pas prendre cela pour acquis "out of the box".


-3

Essayez d’utiliser plusieurs marteaux pour écraser un groupe de clous très rapprochés en même temps, sans aucune communication entre ceux qui tiennent les marteaux ... (supposons qu’ils ont les yeux bandés).

Escalader ceci pour construire une maison.

Il ne vous reste plus qu'à dormir la nuit en vous imaginant architecte. :)


-3

Partie facile: utilisez le multithreading avec les fonctionnalités modernes des frameworks, des systèmes d’exploitation et du matériel, telles que les sémaphores, les files d’attente, les compteurs verrouillés, les types de boîtes atomiques, etc.

Partie difficile: implémenter les fonctionnalités elles-mêmes en n'utilisant aucune fonctionnalité en premier lieu, peut être sauf quelques fonctionnalités matérielles très limitées, en s'appuyant uniquement sur des garanties de cohérence d'horloge sur plusieurs cœurs.


3
La partie difficile est certes plus difficile, mais même cette partie facile n’est pas si facile.
PeterAllenWebb
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.