Au sein de l'entreprise, nous nous efforçons toujours d'augmenter la maintenabilité de notre code, en utilisant des pratiques généralement acceptées, y compris en matière de multithreading. Cela ne résout pas toutes les difficultés qu'une charge sans cesse croissante apporte, mais cela simplifie la prise en charge - il gagne également la lisibilité du code et la vitesse de développement de nouvelles fonctionnalités.
Nous avons maintenant 47 000 utilisateurs par jour, environ 30 serveurs en production, 2 000 demandes d'API par seconde et des versions quotidiennes. Le service Miro se développe depuis 2011 et dans l'implémentation actuelle, les demandes des utilisateurs sont traitées en parallèle par un cluster de serveurs hétérogènes.

Sous-système de contrôle d'accès concurrentiel
La principale valeur de notre produit réside dans les tableaux de bord collaboratifs, de sorte que le principal fardeau leur incombe. Le sous-système principal qui contrôle la plupart des accès concurrentiels est le système dynamique de sessions utilisateur sur la carte.
Pour chaque carte ouvrable sur l'un des serveurs, l'état augmente. Il stocke à la fois les données d'exécution des applications nécessaires pour assurer la collaboration et l'affichage du contenu, ainsi que les données système, telles que la liaison aux threads de traitement. Les informations sur le serveur dans lequel l'état est stocké sont écrites dans une structure distribuée et sont accessibles au cluster tant que le serveur est en cours d'exécution et qu'au moins un utilisateur est sur la carte. Nous utilisons Hazelcast pour fournir cette partie du sous-système. Toutes les nouvelles connexions à la carte sont envoyées au serveur avec cet état.
Lors de la connexion au serveur, l'utilisateur entre dans le flux de réception, dont la seule tâche est de lier la connexion à l'état de la carte correspondante, dans les flux desquels tous les travaux ultérieurs auront lieu.
Deux flux sont associés à la carte: réseau, traitement des connexions et «métier», responsable de la logique métier. Cela vous permet de transformer l'exécution de tâches hétérogènes de traitement de paquets réseau et d'exécution de commandes métier de série en parallèle. Les commandes réseau traitées des utilisateurs forment des tâches métier appliquées et les dirigent vers le flux métier, où elles sont traitées séquentiellement. Cela évite une synchronisation inutile lors du développement du code d'application.
La division du code en entreprise / application et système est notre convention interne. Il vous permet de distinguer le code responsable des fonctionnalités et des capacités des utilisateurs des détails de bas niveau de communication, de délestage et de stockage, qui sont l'outil de service.
Si le flux de réception détecte qu'il n'y a pas d'état pour la carte, la tâche d'initialisation correspondante est définie. L'initialisation de l'état est gérée par un type de thread distinct.
Les types de tâches et leur direction peuvent être représentés comme suit:

Une telle implémentation nous permet de résoudre les problèmes suivants:
- Il n'y a aucune logique métier dans le flux de réception qui pourrait ralentir la nouvelle connexion. Ce type de thread sur le serveur existe en une seule copie, donc les retards affecteront immédiatement l'heure d'ouverture des cartes, et s'il y a une erreur dans le code d'entreprise, vous pouvez facilement le bloquer.
- L'initialisation de l'état n'est pas effectuée dans le flux métier des cartes et n'affecte pas le temps de traitement des commandes métier des utilisateurs. Cela peut prendre un certain temps, et les flux commerciaux traitent plusieurs conseils à la fois, de sorte que l'ouverture de nouveaux conseils n'affecte pas directement les conseils existants.
- L'analyse des commandes réseau est souvent plus rapide que leur exécution directe, de sorte que la configuration du pool de threads réseau peut être différente de la configuration du pool de threads métier afin d'utiliser efficacement les ressources système.
Coloration fluide
Le sous-système décrit ci-dessus dans la mise en œuvre est tout à fait non trivial. Le développeur doit garder à l'esprit le schéma du système et prendre en compte le processus inverse de fermeture des cartes. Lors de la fermeture, il est nécessaire de supprimer tous les abonnements, de supprimer les entrées des registres et de le faire dans les mêmes flux dans lesquels ils ont été initialisés.
Nous avons remarqué que les bogues et les difficultés de modification du code survenus dans ce sous-système étaient souvent associés à un manque de compréhension du contexte d'exécution. Jongler avec les threads et les tâches a rendu difficile la réponse à la question de savoir quel thread particulier un morceau de code particulier exécute.
Pour résoudre ce problème, nous avons utilisé la méthode de coloration des fils - il s'agit d'une politique visant à réglementer l'utilisation des fils dans le système. Les couleurs sont affectées aux threads et les méthodes définissent la portée de l'exécution dans les threads. La couleur est ici une abstraction, il peut s'agir de n'importe quelle entité, par exemple une énumération. En Java, les annotations peuvent servir de langage de marquage des couleurs:
@Color @IncompatibleColors @AnyColor @Grant @Revoke
Des annotations sont ajoutées à la méthode, en les utilisant, vous pouvez définir la validité de la méthode. Par exemple, si l'annotation d'une méthode autorise le jaune et le rouge, le premier thread peut appeler la méthode et pour le second, un tel appel sera erroné.

Des couleurs non valides peuvent être spécifiées:

Vous pouvez ajouter et supprimer des privilèges de thread dans la dynamique:

L'absence d'annotation ou d'annotation comme dans l'exemple ci-dessous indique que la méthode peut être exécutée dans n'importe quel thread:

Les développeurs Android peuvent être familiarisés avec cette approche pour les annotations MainThread, UiThread, WorkerThread, etc.
La coloration des fils utilise le principe du code auto-documenté, et la méthode elle-même se prête bien à l'analyse statique. En utilisant l'analyse statique, vous pouvez dire avant l'exécution du code qu'il est correctement écrit ou non. Si nous excluons les annotations Grant et Revoke et supposons que le flux lors de l'initialisation dispose déjà d'un ensemble de privilèges immuables, alors ce sera une analyse insensible au flux - une version simple de l'analyse statique qui ne prend pas en compte l'ordre des appels.
Au moment de la mise en œuvre de la méthode de coloration des flux, il n'y avait pas de solutions prêtes à l'emploi pour l'analyse statique dans notre infrastructure Devops, nous avons donc opté pour la méthode la plus simple et la moins chère - nous avons introduit nos annotations, qui sont associées de manière unique à chaque type de flux. Nous avons commencé à vérifier leur exactitude à l'aide d'aspects lors de l'exécution.
@Aspect public class ThreadAnnotationAspect { @Pointcut("if()") public static boolean isActive() { …
Pour les aspects, nous utilisons la bibliothèque aspectj et le plugin maven, qui fournit le tissage lors de la compilation du projet. Le tissage a été initialement configuré pour charger-temps lors du chargement des classes avec ClassLoader. Cependant, nous étions confrontés au fait que le tisserand se comportait parfois de manière incorrecte lors du chargement concurrentiel de la même classe, de sorte que le code source de la classe restait inchangé. En conséquence, cela a entraîné un comportement de production très imprévisible et difficile à reproduire. Peut-être que dans les versions actuelles de la bibliothèque, ce problème n'existe pas.
La solution sur les aspects a rapidement trouvé la plupart des problèmes dans le code.
Il est important de ne pas oublier de toujours garder les annotations à jour: elles peuvent être supprimées, ajouter de la paresse, les aspects de tissage peuvent être complètement désactivés - dans ce cas, la coloration perdra rapidement sa pertinence et sa valeur.
Guardedby
L'une des variétés de coloration est l'annotation GuardedBy de java.util.concurrent. Il délimite l'accès aux champs et aux méthodes, indiquant quels verrous sont nécessaires pour un accès correct.
public class PrivateLock { private final Object lock = Object(); @GuardedBy (“lock”) Widget widget; void method() { synchronized (lock) {
Les IDE modernes prennent même en charge l'analyse de cette annotation. Par exemple, IDEA affiche ce message si quelque chose ne va pas avec le code:
La méthode de coloration des threads n'est pas nouvelle, mais il semble que dans des langages tels que Java, où l'accès multi-thread va souvent à des objets mutables, son utilisation non seulement dans le cadre de la documentation, mais également au stade de la compilation, l'assemblage pourrait grandement simplifier le développement de code multi-thread.
Nous utilisons toujours l'implémentation sur les aspects. Si vous connaissez une solution ou un outil d'analyse plus élégant qui vous permet d'augmenter la stabilité de cette approche des modifications du système, veuillez la partager dans les commentaires.