Illustration du célèbre «problème des philosophes dinning»

Accès simultané vs boucle d'événement vs boucle d'événement + accès simultané

Tout d'abord, expliquons la terminologie.
Concurrence - signifie que vous avez plusieurs files de tâches sur plusieurs cœurs / threads de processeur. Mais c’est complètement différent de l’exécution en parallèle, l’exécution en parallèle ne contiendrait pas plusieurs files de file d’attente pour le cas parallèle. Nous aurons besoin d’un cœur / thread de processeur par tâche pour une exécution complète en parallèle, que nous ne pouvons pas définir dans la plupart des cas. C’est pourquoi, pour le développement de logiciels modernes, la programmation parallèle signifie parfois «simultanéité». Je sais que c’est étrange, mais c’est évidemment ce que nous avons pour le moment (cela dépend du modèle de processeur / thread d’OS).
Boucle d'événement - signifie un cycle infini à un seul thread qui crée une tâche à la fois et non seulement une file d'attente à une tâche, mais également une priorité, car avec la boucle d'événement, vous ne disposez que d'une seule ressource pour l'exécution (1 thread). certaines tâches tout de suite vous avez besoin de hiérarchiser les tâches. En quelques mots, cette approche de programmation s'appelle Thread Safe Programming car une seule tâche / fonction / opération ne peut être exécutée à la fois et si vous modifiez quelque chose, cela le serait déjà lors de la prochaine exécution de la tâche.

Programmation simultanée

Dans les ordinateurs / serveurs modernes, nous avons au moins 2 cœurs de processeur et min. 4 threads de la CPU. Mais sur les serveurs maintenant avg. serveur ont au moins 16 threads de processeur. Donc, si vous écrivez un logiciel qui a besoin de performances, vous devez absolument envisager de le faire de manière à ce qu'il utilise tous les cœurs de processeur disponibles sur le serveur.

Cette image affiche le modèle de base de la concurrence, mais il n’est pas si facile de l’afficher :)

La programmation simultanée devient très difficile avec certaines ressources partagées, par exemple, jetons un coup d'œil à ce code concomitant simple et intuitif.

// Mauvaise concurrence avec le langage Go
paquet principal
importation (
   "fmt"
   "temps"
)
var SharedMap = make (map [chaîne] chaîne)
func changeMap (chaîne de valeur) {
    SharedMap ["test"] = valeur
}
func main () {
    allez changeMap ("valeur1")
    allez changeMap ("valeur2")
    time.Sleep (time.Millisecond * 500)
    fmt.Println (SharedMap ["test"])
}
// Ceci affichera "valeur1" ou "valeur2", nous ne le savons pas exactement!

Dans ce cas, Go déclenchera probablement 2 tâches simultanées sur différents cœurs de processeur et nous ne pouvons pas prédire lequel sera exécuté en premier. Nous ne saurions donc pas ce qui sera affiché à la fin.
Pourquoi? - C'est simple! Nous planifions 2 tâches différentes sur différents cœurs de processeur, mais ils utilisent une seule variable / mémoire partagée. Ils sont donc tous deux en train de modifier cette mémoire. Dans certains cas, il s'agirait d'un crash ou d'une exception du programme.

Donc, pour prédire l'exécution de la programmation par accès simultané, nous devons utiliser certaines fonctions de verrouillage telles que Mutex. Grâce à elle, nous pouvons verrouiller cette ressource de mémoire partagée et la rendre disponible pour une tâche à la fois.
Ce style de programmation s'appelle Bloquer parce que nous bloquons toutes les tâches jusqu'à ce que la tâche en cours soit terminée avec la mémoire partagée.

La plupart des développeurs n’apprécient pas la programmation concurrente car la simultanéité ne signifie pas toujours performance. Cela dépend de cas spécifiques.

Boucle d'événement à thread unique

Cette approche de développement logiciel est bien plus simple que la programmation simultanée. Parce que le principe est très simple. Vous n'avez qu'une exécution de tâche à la fois. Et dans ce cas, vous n’avez aucun problème avec les variables / mémoire partagées, car le programme est plus prévisible avec une seule tâche à la fois.

Le flux général suit
1. Event Emitter ajoutant une tâche à la file d’événements à exécuter lors du prochain cycle de la boucle
2. Tâche d’obtention de boucle d’événement de la file d’événements et traitement de cette tâche en fonction de gestionnaires

Permet d'écrire le même exemple avec node.js

laissez SharedMap = {};
const changeMap = (valeur) => {
    return () => {
        SharedMap ["test"] = valeur
    }
}
// 0 Timeout signifie que nous créons une nouvelle tâche en file d'attente pour le prochain cycle.
setTimeout (changeMap ("valeur1"), 0);
setTimeout (changeMap ("valeur2"), 0);
setTimeout (() => {
   console.log (SharedMap ["test"])
}, 500);
// dans ce cas, Node.js affichera "valeur2" car il s'agit d'un fichier unique
// threaded et il a "seulement une file d'attente de tâches"

Comme vous pouvez l’imaginer dans ce cas, le code est beaucoup plus prévisible qu’avec l’exemple Go concurrent, et c’est parce que Node.js s’exécute dans un seul mode threadé à l’aide de la boucle d’événement JavaScript.

Dans certains cas, la boucle d'événements offre plus de performances qu'avec la simultanéité, en raison du comportement non bloquant. Les applications réseau constituent un très bon exemple, car elles utilisent une seule ressource de connexion réseau et ne traitent les données que si elles sont disponibles à l'aide de boucles d'événements Thread Safe.

Concurrence + Boucle d'événements - Pool de threads avec sécurité des threads

Rendre les applications simultanées peut être très difficile, car les bugs liés à la corruption de la mémoire sont omniprésents ou simplement votre application bloque les actions pour chaque tâche. Surtout si vous voulez obtenir une performance maximale, vous devez combiner les deux!

Jetons un coup d'œil sur le modèle de pool de threads et de boucle d'événement de la structure de serveur Web Nginx

Le traitement principal de la mise en réseau et de la configuration est effectué par Worker Event Loop dans un seul thread pour des raisons de sécurité, mais lorsque Nginx doit lire un fichier ou doit traiter les en-têtes / corps de requêtes HTTP, qui bloquent les opérations, il envoie cette tâche à son pool de threads. pour le traitement simultané. Et lorsque la tâche est terminée, le résultat est renvoyé à la boucle d'événements pour que le traitement exécuté du thread safe soit exécuté.

Donc, en utilisant cette structure, vous obtenez à la fois la sécurité des threads et la concurrence, ce qui permet d’utiliser tous les cœurs de processeur pour améliorer les performances et de conserver le principe de non blocage avec une boucle d’événement à thread unique.

Conclusion

Un grand nombre de logiciels sont écrits avec pure concurrence ou avec une boucle d’événement pur à thread unique, mais en combinant les deux dans une seule application, il est ainsi plus facile d’écrire des applications performantes et d’utiliser toutes les ressources de processeur disponibles.