Java et C # assurent la sécurité de la mémoire en vérifiant les limites des tableaux et les déréférences des pointeurs.
Il est important de réfléchir d'abord à la manière dont C # et Java procèdent. Ils le font en convertissant ce qui est un comportement indéfini en C ou C ++ en comportement défini: planter le programme . Les déréférences nulles et les exceptions d'index de tableau ne doivent jamais être interceptées dans un programme C # ou Java correct; ils ne devraient pas se produire en premier lieu parce que le programme ne devrait pas avoir ce bogue.
Mais je pense que ce n'est pas ce que vous entendez par votre question! Nous pourrions assez facilement écrire un runtime "deadlock safe" qui vérifie périodiquement s'il y a n threads qui s'attendent mutuellement et terminent le programme si cela se produit, mais je ne pense pas que cela vous satisfasse.
Quels mécanismes pourraient être mis en œuvre dans un langage de programmation pour éviter la possibilité de conditions de concurrence et de blocages?
Le problème suivant auquel nous sommes confrontés avec votre question est que les "conditions de concurrence", contrairement aux blocages, sont difficiles à détecter. N'oubliez pas que ce que nous recherchons en matière de sécurité des threads n'est pas d' éliminer les courses . Ce que nous voulons, c'est que le programme soit correct, peu importe qui remporte la course ! Le problème avec les conditions de concurrence n'est pas que deux threads s'exécutent dans un ordre indéfini et nous ne savons pas qui va terminer en premier. Le problème avec les conditions de concurrence est que les développeurs oublient que certaines commandes de finition de threads sont possibles et ne tiennent pas compte de cette possibilité.
Donc, votre question se résume essentiellement à "y a-t-il un moyen pour un langage de programmation de s'assurer que mon programme est correct?" et la réponse à cette question est, en pratique, non.
Jusqu'à présent, je n'ai fait que critiquer votre question. Permettez-moi d'essayer de changer de vitesse ici et de répondre à l'esprit de votre question. Existe-t-il des choix que les concepteurs de langage pourraient faire pour atténuer la situation horrible dans laquelle nous nous trouvons avec le multithreading?
La situation est vraiment horrible! Obtenir un code multithread correct, en particulier sur des architectures de modèle de mémoire faible, est très, très difficile. Il est instructif de réfléchir aux raisons de la difficulté:
- Il est difficile de raisonner sur plusieurs threads de contrôle en un seul processus. Un fil est assez dur!
- Les abstractions deviennent extrêmement fuyantes dans un monde multithread. Dans le monde à thread unique, nous sommes garantis que les programmes se comportent comme s'ils étaient exécutés dans l'ordre, même s'ils ne sont pas réellement exécutés dans l'ordre. Dans le monde multithread, ce n'est plus le cas; les optimisations qui seraient invisibles sur un seul thread deviennent visibles, et maintenant le développeur doit comprendre ces optimisations possibles.
- Mais ça empire. La spécification C # indique qu'une implémentation n'est PAS obligée d'avoir un ordre cohérent de lectures et d'écritures qui peut être accepté par tous les threads . L'idée qu'il y a du tout des "courses" et qu'il y a un vainqueur clair, n'est en fait pas vraie! Considérez une situation où il y a deux écritures et deux lectures dans certaines variables sur plusieurs threads. Dans un monde sensé, nous pourrions penser "eh bien, nous ne pouvons pas savoir qui va gagner les courses, mais au moins il y aura une course et quelqu'un gagnera". Nous ne sommes pas dans ce monde sensible. C # permet à plusieurs threads d' être en désaccord sur l'ordre dans lequel les lectures et les écritures se produisent; il n'y a pas nécessairement un monde cohérent que tout le monde observe.
Il existe donc un moyen évident pour les concepteurs de langage d'améliorer les choses. Abandonnez les gains de performances des processeurs modernes . Assurez-vous que tous les programmes, même ceux à plusieurs threads, ont un modèle de mémoire extrêmement solide. Cela rendra les programmes multithread beaucoup, beaucoup plus lents, ce qui va directement à l'encontre de la raison d'avoir des programmes multithread en premier lieu: pour de meilleures performances.
Même en laissant de côté le modèle de mémoire, il existe d'autres raisons pour lesquelles le multithreading est difficile:
- La prévention des blocages nécessite une analyse de l'ensemble du programme; vous devez connaître l'ordre global dans lequel les verrous peuvent être supprimés et appliquer cet ordre à l'ensemble du programme, même si le programme est composé de composants écrits à différents moments par différentes organisations.
- Le principal outil que nous vous donnons pour apprivoiser le multithreading est le verrou, mais les verrous ne peuvent pas être composés .
Ce dernier point mérite une explication supplémentaire. Par "composable", j'entends ce qui suit:
Supposons que nous souhaitons calculer un int donné un double. Nous écrivons une implémentation correcte du calcul:
int F(double x) { correct implementation here }
Supposons que nous souhaitons calculer une chaîne avec un int:
string G(int y) { correct implementation here }
Maintenant, si nous voulons calculer une chaîne avec un double:
double d = whatever;
string r = G(F(d));
G et F peuvent être composés en une solution correcte au problème plus complexe.
Mais les serrures n'ont pas cette propriété à cause des blocages. Une méthode correcte M1 qui prend des verrous dans l'ordre L1, L2 et une méthode correcte M2 qui prend des verrous dans l'ordre L2, L1, ne peuvent pas toutes les deux être utilisées dans le même programme sans créer un programme incorrect. Les verrous font en sorte que vous ne pouvez pas dire "chaque méthode individuelle est correcte, donc tout est correct".
Alors, que pouvons-nous faire, en tant que concepteurs linguistiques?
D'abord, n'y allez pas. Plusieurs threads de contrôle dans un programme sont une mauvaise idée, et le partage de mémoire entre les threads est une mauvaise idée, donc ne le mettez pas dans la langue ou le runtime en premier lieu.
Il s'agit apparemment d'un non-démarreur.
Tournons ensuite notre attention vers la question la plus fondamentale: pourquoi avons-nous en premier lieu plusieurs fils? Il y a deux raisons principales, et elles sont souvent confondues dans la même chose, bien qu'elles soient très différentes. Ils sont confondus car ils concernent tous deux la gestion de la latence.
- Nous créons des threads, à tort, pour gérer la latence des E / S. Besoin d'écrire un gros fichier, d'accéder à une base de données distante, peu importe, de créer un thread de travail plutôt que de verrouiller votre thread d'interface utilisateur.
Mauvaise idée. Au lieu de cela, utilisez une asynchronie à thread unique via les coroutines. C # le fait magnifiquement. Java, pas si bien. Mais c'est le principal moyen que la génération actuelle de concepteurs de langage aide à résoudre le problème de threading. L' await
opérateur en C # (inspiré des workflows asynchrones F # et autres antériorités) est en cours d'intégration dans de plus en plus de langages.
- Nous créons des threads, correctement, pour saturer les processeurs inactifs avec un travail de calcul lourd. Fondamentalement, nous utilisons des threads comme processus légers.
Les concepteurs de langage peuvent vous aider en créant des fonctionnalités de langage qui fonctionnent bien avec le parallélisme. Pensez à la façon dont LINQ est étendu si naturellement à PLINQ, par exemple. Si vous êtes une personne sensée et que vous limitez vos opérations TPL à des opérations liées au processeur qui sont très parallèles et ne partagent pas la mémoire, vous pouvez obtenir de gros gains ici.
Que pouvons-nous faire d'autre?
- Faire en sorte que le compilateur détecte les erreurs les plus osées et les transforme en avertissements ou erreurs.
C # ne vous permet pas d'attendre dans une serrure, car c'est une recette pour les blocages. C # ne vous permet pas de verrouiller un type de valeur car c'est toujours la mauvaise chose à faire; vous verrouillez la boîte, pas la valeur. C # vous avertit si vous alias un volatile, car l'alias n'impose pas de sémantique d'acquisition / libération. Il existe de nombreuses autres façons pour le compilateur de détecter les problèmes courants et de les éviter.
- Concevez des fonctionnalités «fosse de qualité», où la façon la plus naturelle de le faire est aussi la plus correcte.
C # et Java ont fait une énorme erreur de conception en vous permettant d'utiliser n'importe quel objet de référence comme moniteur. Cela encourage toutes sortes de mauvaises pratiques qui rendent plus difficile le repérage des blocages et plus difficile à éviter statiquement. Et cela gaspille des octets dans chaque en-tête d'objet. Les moniteurs doivent provenir d'une classe de moniteurs.
- Beaucoup de temps et d'efforts ont été consacrés à la recherche Microsoft pour ajouter de la mémoire transactionnelle logicielle à un langage de type C #, et ils n'ont jamais réussi à le faire fonctionner suffisamment bien pour l'incorporer dans le langage principal.
STM est une belle idée, et j'ai joué avec des implémentations de jouets dans Haskell; il vous permet de composer de manière beaucoup plus élégante des solutions correctes à partir de pièces correctes que les solutions basées sur les verrous. Cependant, je ne connais pas suffisamment les détails pour expliquer pourquoi il n'a pas été possible de travailler à grande échelle; demandez à Joe Duffy la prochaine fois que vous le verrez.
- Une autre réponse a déjà mentionné l'immuabilité. Si vous avez l'immuabilité combinée à des coroutines efficaces, vous pouvez créer des fonctionnalités comme le modèle d'acteur directement dans votre langage; pense Erlang, par exemple.
Il y a eu beaucoup de recherches sur les langages basés sur le calcul de processus et je ne comprends pas très bien cet espace; essayez de lire vous-même quelques articles et voyez si vous avez des idées.
- Facilitez la tâche des tiers pour écrire de bons analyseurs
Après avoir travaillé chez Microsoft sur Roslyn, j'ai travaillé chez Coverity, et l'une des choses que j'ai faites a été d'obtenir l'interface utilisateur de l'analyseur en utilisant Roslyn. En ayant une analyse lexicale, syntaxique et sémantique précise fournie par Microsoft, nous pourrions alors nous concentrer sur le dur travail d'écriture des détecteurs qui ont trouvé des problèmes communs de multithreading.
- Augmenter le niveau d'abstraction
Une raison fondamentale pour laquelle nous avons des races et des blocages et tout ça, c'est parce que nous écrivons des programmes qui disent quoi faire , et il se trouve que nous sommes tous des conneries à écrire des programmes impératifs; l'ordinateur fait ce que vous lui dites et nous lui demandons de faire les mauvaises choses. De nombreux langages de programmation modernes sont de plus en plus sur la programmation déclarative: dites quels résultats vous voulez, et laissez le compilateur trouver la manière efficace, sûre et correcte pour atteindre ce résultat. Encore une fois, pensez à LINQ; nous voulons que vous disiez from c in customers select c.FirstName
, qui exprime une intention . Laissez le compilateur comprendre comment écrire le code.
- Utilisez des ordinateurs pour résoudre des problèmes informatiques.
Les algorithmes d'apprentissage automatique sont bien meilleurs pour certaines tâches que les algorithmes codés à la main, bien qu'il y ait bien sûr de nombreux compromis, notamment la correction, le temps de formation, les biais introduits par une mauvaise formation, etc. Mais il est probable qu'un grand nombre de tâches que nous codons actuellement "à la main" pourront bientôt faire l'objet de solutions générées par machine. Si les humains n'écrivent pas le code, ils n'écrivent pas de bogues.
Désolé, c'était un peu décousu; c'est un sujet énorme et difficile et aucun consensus clair n'a émergé dans la communauté PL au cours des 20 années que j'ai suivies les progrès dans cet espace problématique.