La synchronicité est un mythe

Bonjour à tous!

Aujourd'hui, vous trouverez un long texte sans images (légèrement raccourci par rapport à l'original), où la thèse présentée dans la rubrique est analysée en détail. Le vétéran de Microsoft, Terry Crowley, décrit l'essence de la programmation asynchrone et explique pourquoi cette approche est beaucoup plus réaliste et plus appropriée que synchrone et séquentielle.

Ceux qui souhaitent ou pensent écrire un livre qui aborde de tels sujets - écrivez à titre personnel.

La synchronicité est un mythe. Rien ne se produit instantanément. Tout prend du temps.
Certaines caractéristiques des systèmes informatiques et des environnements de programmation sont fondamentalement basées sur le fait que les calculs se produisent dans le monde physique tridimensionnel et sont limitées par les limites de la vitesse de la lumière et les lois de la thermodynamique.

Un tel enracinement dans le monde physique signifie que certains aspects ne perdent pas leur pertinence même avec l'avènement de nouvelles technologies qui offrent de nouvelles opportunités et un accès à de nouvelles frontières de productivité. Ils restent valables car ils ne sont pas seulement des «options choisies lors de la conception», mais la réalité sous-jacente de l'Univers physique.

La différence entre le synchronisme et l'asynchronie dans le langage et la création de systèmes est un tel aspect de la conception qui a des fondements physiques profonds. La plupart des programmeurs commencent immédiatement à travailler avec de tels programmes et langages, où une exécution synchrone est impliquée. En fait, c'est tellement naturel que personne ne le mentionne ni n'en parle directement. Le terme «synchrone» dans ce contexte signifie que le calcul a lieu immédiatement, comme une série d'étapes successives, et que rien d'autre ne se produit avant qu'il ne soit terminé. J'exécute “c = a + b” “x = f(y)” - et rien d'autre ne se produira jusqu'à la fin de cette instruction.

Bien sûr, rien ne se produit instantanément dans l'Univers physique. Tous les processus sont associés à des retards - vous devez parcourir la hiérarchie de la mémoire, exécuter un cycle de processeur, lire les informations à partir d'un lecteur de disque ou vous connecter à un autre nœud sur le réseau, ce qui entraîne également des retards dans la transmission des données. Tout cela est une conséquence fondamentale de la vitesse de propagation de la lumière et du signal en trois dimensions.

Tous les processus sont un peu en retard, tout prend du temps. En définissant certains processus comme synchrones, nous disons essentiellement que nous allons ignorer ce délai et décrire notre calcul comme instantané. En fait, dans les systèmes informatiques, une infrastructure sérieuse est souvent installée, ce qui vous permet de continuer à utiliser activement le matériel de base, même lorsqu'ils essaient d'optimiser l'interface de programmation en présentant les événements qui s'y produisent comme synchrones.

L'idée même que la synchronisation est fournie à l'aide d'un mécanisme spécial et est associée à des coûts peut sembler illogique au programmeur, qui est plus habitué au fait que c'est l'asynchronie qui nécessite un contrôle externe actif. En fait, c'est ce qui se passe réellement lorsqu'une interface asynchrone est fournie: une véritable asynchronie fondamentale s'ouvre pour le programmeur un peu plus clairement qu'auparavant, et il doit la traiter manuellement, plutôt que de s'appuyer sur un programme qui pourrait le faire automatiquement. La fourniture directe d'asynchronie est associée à des coûts supplémentaires pour le programmeur, mais vous permet en même temps de répartir plus efficacement les coûts et les compromis inhérents à ce domaine, et de ne pas laisser cela à la merci d'un système qui équilibrerait ces coûts et ces compromis. L'interface asynchrone correspond souvent plus précisément aux événements qui se produisent physiquement dans le système de base et, en conséquence, ouvre des possibilités d'optimisation supplémentaires.

Par exemple, le processeur et le système de mémoire sont dotés d'une infrastructure équitable chargée de lire et d'écrire des données en mémoire, en tenant compte de sa hiérarchie. Au niveau 1 (L1), le lien de cache peut prendre plusieurs nanosecondes, tandis que le lien de mémoire lui-même doit traverser L2, L3 et la mémoire principale, ce qui peut prendre des centaines de nanosecondes. Si vous attendez juste que la liaison mémoire soit résolue, le processeur sera inactif pendant un pourcentage significatif du temps.

Des mécanismes sérieux sont utilisés pour optimiser de tels phénomènes: pipelining avec une vue dominante du flux de commandes, plusieurs opérations simultanées de récupération de la mémoire et du stockage de données actuel, prédiction de branche et tentatives d'optimiser davantage le programme, même lorsqu'il passe à un autre emplacement de mémoire, contrôle précis des barrières de mémoire pour garantir que tout ce mécanisme complexe continuera à fournir un modèle de mémoire cohérent pour un environnement de programmation de niveau supérieur. Toutes ces choses sont faites dans un effort pour optimiser les performances et utiliser au maximum le matériel pour masquer ces retards de 10 à 100 nanosecondes dans la hiérarchie de la mémoire et fournir un système dans lequel une exécution synchrone est censée se produire, tout en réduisant les performances décentes du cœur du processeur.

Il est loin d'être toujours clair à quel point ces optimisations sont efficaces pour un morceau de code particulier, et des outils très spécifiques pour analyser les performances sont souvent nécessaires pour répondre à cette question. Un tel travail analytique est prévu dans le développement de quelques codes très précieux (par exemple, comme dans le moteur de conversion pour Excel, certaines options de compression dans le noyau, ou des chemins cryptographiques dans le code).

Les opérations avec un retard plus important, par exemple la lecture de données à partir d'un disque rotatif, nécessitent l'utilisation d'autres mécanismes. Dans de tels cas, lors de la demande de lecture à partir du disque du système d'exploitation, il sera nécessaire de basculer complètement vers un autre thread ou processus, et la demande synchrone restera non envoyée. Les coûts élevés de commutation et de prise en charge de ce mécanisme en tant que tels sont acceptables, car la latence cachée dans ce cas peut atteindre plusieurs millisecondes plutôt que des nanosecondes. Remarque: ces coûts ne se réduisent pas à la simple commutation entre les threads, mais incluent le coût de toute la mémoire et des ressources, qui restent en fait inactives jusqu'à la fin de l'opération. Tous ces coûts doivent aller pour fournir une interface supposée synchrone.

Il existe un certain nombre de raisons fondamentales pour lesquelles il peut être nécessaire de révéler la véritable asynchronie de base dans le système et pour lesquelles il serait préférable d'utiliser une interface asynchrone avec un certain composant, niveau ou application, même en tenant compte de la nécessité de faire face directement à la complexité croissante.

Accès simultané Si la ressource fournie est conçue pour un véritable parallélisme, l'interface asynchrone permet au client d'émettre plus naturellement simultanément plusieurs requêtes et de les gérer, pour utiliser pleinement les ressources de base.

Convoyeur . La manière habituelle de réduire le retard réel sur certaines interfaces est de s'assurer que plusieurs demandes attendent d'être envoyées à un moment donné (la mesure dans laquelle cela est réellement utile en termes de performances dépend de l'endroit où nous obtenons la source du retard). Dans tous les cas, si le système est adapté pour le pipeline, le retard réel peut être réduit d'un facteur égal au nombre de demandes en attente d'être envoyées. Ainsi, cela peut prendre 10 ms pour terminer une demande spécifique, mais si vous écrivez 10 demandes dans le pipeline, la réponse peut arriver toutes les millisecondes. Le débit total est fonction de la canalisation disponible, et pas seulement d'un délai de «transmission» par demande. Une interface synchrone qui émet une demande et attend une réponse donnera toujours un délai de bout en bout plus élevé.

Emballage (local ou distant) . L'interface asynchrone fournit plus naturellement l'implémentation d'un système de conditionnement de requêtes, soit localement, soit sur une ressource distante (remarque: dans ce cas, le «disque» à l'autre extrémité de l'interface d'E / S peut être «distant»). Le fait est que l'application doit déjà faire face à la réception de la réponse, et en même temps, il y aura un certain retard, car l'application n'interrompra pas le traitement en cours. Ce traitement supplémentaire peut être associé à des demandes supplémentaires qui seraient naturellement regroupées.

Le traitement par lots local peut fournir un transfert plus efficace de séries de demandes, voire la compression et la suppression des demandes en double directement sur la machine locale. Pour pouvoir accéder simultanément à tout un ensemble de requêtes sur une ressource distante, une optimisation sérieuse peut être requise. Un exemple classique: un contrôleur de disque réorganise une série d'opérations de lecture et d'écriture afin de profiter de la position de la tête de disque sur une plaque tournante et de minimiser le temps d'avance de la tête. Sur toute interface de stockage de données fonctionnant au niveau du bloc, vous pouvez sérieusement améliorer les performances en regroupant une série de requêtes dans lesquelles toutes les opérations de lecture et d'écriture tombent sur le même bloc.

Naturellement, le packaging local peut également être implémenté sur une interface synchrone, mais pour cela, vous devrez soit «cacher la vérité» dans une large mesure, soit programmer le regroupement de packages en tant que fonctionnalité spéciale de l'interface, ce qui peut compliquer considérablement le client tout entier. Un exemple classique de dissimulation de la vérité est les E / S tamponnées. L'application appelle “write(byte)” , et l'interface renvoie le success , mais, en fait, l'enregistrement lui-même (ainsi que des informations sur le fait qu'il a réussi) n'aura lieu que lorsque le tampon est explicitement rempli ou vide, et cela se produit lorsque le fichier est fermé . De nombreuses applications peuvent ignorer de tels détails - un gâchis ne se produit que lorsque l'application doit garantir certaines séquences d'opérations en interaction, ainsi qu'une véritable idée de ce qui se passe aux niveaux inférieurs.

Déverrouillez / Libérez . L'une des utilisations les plus courantes de l'asynchronie dans le contexte des interfaces utilisateur graphiques est d'empêcher le thread principal de l'interface utilisateur d'être bloqué afin que l'utilisateur puisse continuer à interagir avec l'application. Les retards dans les opérations à long terme (telles que les communications réseau) ne peuvent pas être cachés derrière une interface synchrone. Dans ce cas, le thread d'interface utilisateur doit gérer explicitement ces opérations asynchrones et faire face à la complexité supplémentaire qui est introduite dans le programme.

L'interface utilisateur n'est qu'un exemple dans lequel le composant doit continuer à répondre à des demandes supplémentaires et, par conséquent, ne peut pas s'appuyer sur un mécanisme standard qui masque les retards afin de simplifier le travail du programmeur.
Un composant de serveur Web qui reçoit de nouvelles connexions aux sockets transférera, en règle générale, très rapidement une telle connexion à un autre composant asynchrone qui fournit la communication sur le socket, et reviendra lui-même au traitement des nouvelles requêtes.

Dans les modèles synchrones, les composants et leurs modèles de traitement sont généralement étroitement liés.
Les interactions asynchrones sont un mécanisme souvent utilisé pour desserrer la liaison .

Réduction et gestion des coûts. Comme mentionné ci-dessus, tout mécanisme pour masquer l'asynchronie implique une allocation de ressources et des frais généraux. Pour une application particulière, une telle surcharge peut ne pas être acceptable et le concepteur de cette application doit trouver un moyen de contrôler l'asynchronie naturelle.

Un exemple intéressant est l'histoire des serveurs Web. Les premiers serveurs Web (basés sur Unix) utilisaient généralement un processus distinct pour gérer les demandes entrantes. Ensuite, ce processus pouvait lire cette connexion et y écrire, cela s'est produit, essentiellement, de manière synchrone. Une telle conception s'est développée et les coûts ont été réduits lorsque les threads ont commencé à être utilisés à la place des processus, mais le modèle d'exécution synchrone global a été préservé. Dans les options de conception modernes, il est reconnu que l'attention principale ne doit pas être accordée au modèle de calcul, mais, tout d'abord, aux entrées / sorties liées à la lecture et à l'écriture lors de l'échange d'informations avec une base de données, d'un système de fichiers ou de la transmission d'informations sur un réseau, tout en formulant une réponse . Habituellement, des files d'attente de travail sont utilisées pour cela, dans lesquelles une certaine limite sur le nombre de threads est autorisée - et dans ce cas, il est possible de construire plus clairement la gestion des ressources.

Le succès de NodeJS dans le développement backend ne s'explique pas seulement par le support de ce moteur du côté de nombreux développeurs JavaScript qui ont grandi en créant des interfaces web client. Dans NodeJS, comme dans les scripts de navigateur, une grande attention est accordée à la conception de manière asynchrone, ce qui va bien avec les options de charge de serveur typiques: la gestion des ressources du serveur dépend principalement des E / S et non du traitement.

Il y a un autre aspect intéressant: ces compromis sont plus explicites et mieux ajustés par le développeur de l'application, si vous adhérez à l'approche asynchrone. Dans l'exemple avec des retards dans la hiérarchie de la mémoire, le retard réel (mesuré en cycles de processeur en termes de requête en mémoire) a considérablement augmenté sur plusieurs décennies. Les développeurs de processeurs ont du mal à ajouter de nouveaux niveaux de cache et des mécanismes supplémentaires qui poussent de plus en plus le modèle de mémoire fourni par le processeur afin de maintenir l'apparence du traitement synchrone.

La commutation de contexte aux frontières des E / S synchrones est un autre exemple où les compromis réels ont radicalement changé au fil du temps. L'augmentation des cycles du processeur est plus rapide que la lutte contre les retards, ce qui signifie que maintenant l'application manque beaucoup plus de capacités de calcul, alors qu'elle est inactive sous une forme verrouillée, en attendant la fin de l'IO. Le même problème lié au coût relatif des compromis a conduit les concepteurs de systèmes d'exploitation à s'en tenir à des schémas de gestion de la mémoire qui sont beaucoup plus similaires aux modèles précédents avec l'échange de processus (où l'image de processus entière est entièrement chargée en mémoire, après quoi le processus démarre), au lieu de l'échange pages. Il est trop difficile de masquer les retards qui peuvent survenir à la bordure de chaque page. Le débit total considérablement amélioré obtenu avec de grandes demandes d'E / S séquentielles (par rapport à l'utilisation de demandes aléatoires) contribue également à ces changements.

Autres sujets

Annuler

L'annulation est un sujet complexe . Historiquement, les systèmes à orientation synchrone ont fait un mauvais travail avec le traitement d'annulation, et certains ne supportaient même pas du tout l'annulation. L'annulation devait essentiellement être conçue «hors bande», pour une telle opération, il fallait appeler un thread d'exécution distinct. Comme alternative, les modèles asynchrones conviennent, où la prise en charge de l'annulation est organisée plus naturellement, en particulier, une telle approche triviale est utilisée: elle ignore simplement quelle réponse retourne finalement (et si elle revient du tout). L'annulation devient de plus en plus importante lorsque la variabilité des retards augmente, et en pratique, le taux d'erreur augmente également - ce qui donne une très bonne tranche historique démontrant comment nos environnements réseau se sont développés.

Limitation / Gestion des ressources

La conception synchrone, par définition, impose une limitation, empêchant l'application d'émettre des demandes supplémentaires jusqu'à la fin de la demande actuelle. Dans une conception asynchrone, la limitation ne se produit pas pour rien, il est donc parfois nécessaire de l'implémenter explicitement. Cet article décrit la situation avec Word Web App à titre d'exemple, où la transition de la conception synchrone à la conception asynchrone a provoqué de graves problèmes de gestion des ressources. Si l'application utilise une interface synchrone, elle risque de ne pas reconnaître que la limitation est implicitement incorporée dans le code. Lors de la suppression de cette limitation implicite, il est possible (ou nécessaire) d'organiser la gestion des ressources de manière plus explicite.

J'ai dû faire face à cela au tout début de ma carrière lorsque nous avons porté un éditeur de texte de l'API graphique synchrone de Sun vers X Windows. Lors de l'utilisation de l'API Sun, l'opération de rendu était synchrone, de sorte que le client ne reprenait le contrôle qu'une fois terminé. Sous X Windows, une requête graphique a été envoyée de manière asynchrone sur une connexion réseau, puis exécutée par le serveur d'affichage (qui pourrait être sur la même machine ou sur une machine différente).

Pour garantir de bonnes performances interactives, notre application doit fournir un certain rendu (c'est-à-dire, s'assurer que la ligne où se trouve maintenant le curseur est mise à jour et rendue), puis vérifier s'il y a une autre entrée de clavier qui doit être lue. , ( , ), , . API. , , - . , . UI , .

, 30 (-, Facebook iPhone ). – ( , ), , . , , .



, . , Microsoft, , API – , , . , , – : «, !» , , .

, . – , . , : , , , . , - . , , async/await . «» , , , JavaScript. : , . Async/await , , . . , , , .

. , , . , , , . , , ( !). () , , .

, . , . async/await, , , , .

, , , – . , . – , , , ( , – Word Excel). , , - , , , .
, , , , .
, – . .

Conclusions

. – , , . , , , . , ; , .

Source: https://habr.com/ru/post/fr423573/


All Articles