Le modèle de concurrence de Vert.x s’articule autour d’autres concepts fondamentaux que sont la programmation réactive, les instructions non bloquantes et enfin l’event loop.
Programmation réactive
Par programmation réactive, nous entendons programmation événementielle, et non cette fois-ci système réactif. Des co-auteurs du manifeste des systèmes réactifs(Jonas Bonér e Viktor Klang) ont écrit un excellent article de clarification entre programmation réactive et systèmes réactifs.
La notion de systèmes réactifs comme nous l’avons vu dans l’introduction est un ensemble de principes de design architecturaux pour la construction de systèmes disponibles, résilients et élastiques. La programmation réactive est un style de programmation asynchrone où l’on écrit des instructions dont l’exécution est déclenchée par l’occurrence d’événements. La programmation réactive est au cœur de Vert.x et nous en avons eu un large aperçu dans notre exemple basique.
Par exemple dans cet extrait du listing 1, nous avions passé au serveur un handler que ce dernier appellera à l’arrivée d’une nouvelle requête :
Dans le listing 3, un handler est également passé à la méthode d’envoi de la requête au Greetingserver :
RxJava s’étant imposé dans le monde Java comme implémentation de référence pour faire de la programmation réactive, Vert.x génère une version de son API compatible avec RxJava. Plus loin dans l’article, une section aborde la programmation réactive dans Vert.x avec RxJava.
Le modèle de concurrence de Vert.x exige également l’utilisation d’instructions non bloquantes.
Instructions non bloquantes
Les instructions bloquantes ne doivent être utilisées qu’en situation d’exception dans Vert.x, et à part justement quelques exceptions, tous les appels de l’API Vert.x sont non bloquants.
Vert.x privilégie l’écriture d’instructions non bloquantes car des threads bloqués dans une application sont des threads qui ne font rien et peuvent conduire à des switchs de contexte couteux.
Pour passer à l’échelle, une application comportant beaucoup d’instructions bloquantes n’a d’autres choix que de multiplier le nombre de threads, une solution au mieux simplement couteuse sur le plan matériel et financier si les moyens le permettent, au pire non réalisable par manque de ressources justement. Ces situations sont illustrées par les figures 2.1 et 2.2.
Mais que propose alors Vert.x lorsque nous avons besoin de faire des appels bloquants ? C’est ce que nous allons voir dans la prochaine section.
Figure 2‑1 Instructions bloquantes (les zones rouges correspondent aux états de blocage)
Figure 2‑2 Instructions bloquantes quand il y a beaucoup de threads
Event loop
Comment gérer du code comportant des instructions bloquantes ? L’idée de la réponse est de découper le code au niveau des appels bloquants en parties appelées continuations. Avec l’exemple de la figure 2.1, cela donnera par exemple le découpage de la figure 2.3. Le thread va donc exécuter la continuation C1, mais vu qu’elle est bloquante, plutôt que d’attendre la fin de l’instruction I/O, le thread va continuer à exécuter un autre bout de code, et à la fin de la continuation C1, l’exécution de la continuation C2 va être lancée et l’information récupérée au niveau de l’I/O de C1 va lui être passée comme événement. Ainsi l’utilisation du thread et par conséquent de la machine est optimale, c’est l’intérêt de la programmation événementielle et des instructions non bloquantes. Le pattern d’exécution correspondant à cet usage du thread est appelé event Loop, ou encore reactor (figure 2.4).
Figure 2‑3 Continuations en fonction des appels bloquants
Figure 2‑4 Event loop
Le pattern reactor de base permet de partager un seul fil d’exécution basé sur un seul thread (figure 2.4) entre différents traitements concurrents. Pour tirer parti des architectures multiprocesseurs et multicœurs, Vert.x implémente une version du pattern reactor permettant de paralléliser les traitements sur plusieurs fils d’exécutions, et désigne cette version par multi-reactor pattern (figure 2.5).
Figure 2‑5 reactor vs Multi-reactor
Après l’explication du rôle et du fonctionnement de l’event loop, il devient encore plus évident pourquoi les instructions bloquantes ne doivent pas y être exécutées ; on bloquerait le thread et empêcherait l’exécution des autres traitements concurrents, on perdrait tout l’avantage de cette architecture réfléchie pour optimiser l’usage des ressources matérielles.
Mais les instructions bloquantes ne se limitent pas aux classiques I/O ou états d’attente des threads (sleep, wait, etc), les opérations dont le temps d’exécution dépasse un certain seuil sont toutes aussi néfastes au bon fonctionnement de l’event loop, elles ont aussi pour conséquence de bloquer l’exécution des autres traitements concurrents que celle-ci doit gérer.
Encore une fois, que faire lorsqu’en réaction justement à un événement résultat d’un traitement bloquant, on doive de nouveau exécuter un autre traitement bloquant ? Eh bien, il faut de nouveau découper ce traitement et lancer l’exécution de la partie bloquante en dehors de l’event loop. Vert.x propose différentes techniques pour cela, qui se ramènent tous cela dit à l’exécution de ces codes dans des threads autres que ceux utilisés pour l’exécution des event loops.