Dans ma nouvelle équipe que je gère, la majorité de notre code est la plate-forme, le socket TCP et le code de réseau http. Tout C ++. La plupart d'entre eux proviennent d'autres développeurs qui ont quitté l'équipe. Les développeurs actuels de l'équipe sont très intelligents, mais surtout juniors en termes d'expérience.
Notre plus gros problème: les bogues de concurrence multithread. La plupart de nos bibliothèques de classes sont écrites pour être asynchrones en utilisant certaines classes de pool de threads. Les méthodes sur les bibliothèques de classes mettent souvent en file d'attente des prises longues sur le pool de threads d'un thread, puis les méthodes de rappel de cette classe sont invoquées sur un autre thread. En conséquence, nous avons beaucoup de bogues de cas de bord impliquant des hypothèses de thread incorrectes. Il en résulte des bogues subtils qui vont au-delà de la simple présence de sections critiques et de verrous pour se prémunir contre les problèmes de concurrence.
Ce qui rend ces problèmes encore plus difficiles, c'est que les tentatives de résolution sont souvent incorrectes. Certaines erreurs que j'ai observées que l'équipe tente (ou dans le code hérité lui-même) incluent quelque chose comme ce qui suit:
Erreur courante # 1 - Résoudre le problème de concurrence en mettant simplement un verrou sur les données partagées, mais en oubliant ce qui se passe lorsque les méthodes ne sont pas appelées dans un ordre attendu. Voici un exemple très simple:
void Foo::OnHttpRequestComplete(statuscode status)
{
m_pBar->DoSomethingImportant(status);
}
void Foo::Shutdown()
{
m_pBar->Cleanup();
delete m_pBar;
m_pBar=nullptr;
}
Nous avons donc maintenant un bug dans lequel Shutdown pourrait être appelé pendant que OnHttpNetworkRequestComplete se produit. Un testeur trouve le bogue, capture le vidage sur incident et attribue le bogue à un développeur. Il corrige à son tour le bogue comme celui-ci.
void Foo::OnHttpRequestComplete(statuscode status)
{
AutoLock lock(m_cs);
m_pBar->DoSomethingImportant(status);
}
void Foo::Shutdown()
{
AutoLock lock(m_cs);
m_pBar->Cleanup();
delete m_pBar;
m_pBar=nullptr;
}
Le correctif ci-dessus semble bon jusqu'à ce que vous réalisiez qu'il existe un boîtier de bord encore plus subtil. Que se passe-t-il si Shutdown est appelé avant que OnHttpRequestComplete ne soit rappelé? Les exemples réels de mon équipe sont encore plus complexes et les cas marginaux sont encore plus difficiles à repérer pendant le processus de révision du code.
Erreur courante n ° 2 - résoudre les problèmes de blocage en sortant aveuglément du verrou, attendre la fin de l'autre thread, puis ressaisir le verrou - mais sans gérer le cas où l'objet vient d'être mis à jour par l'autre thread!
Erreur courante # 3 - Même si les objets sont comptés par référence, la séquence d'arrêt "libère" son pointeur. Mais oublie d'attendre que le thread en cours d'exécution libère son instance. En tant que tels, les composants sont arrêtés proprement, puis des rappels parasites ou tardifs sont invoqués sur un objet dans un état n'attendant plus d'appels.
Il existe d'autres cas de bord, mais la ligne de fond est la suivante:
La programmation multithread est tout simplement difficile, même pour les personnes intelligentes.
Pendant que j'attrape ces erreurs, je passe du temps à discuter des erreurs avec chaque développeur pour développer un correctif plus approprié. Mais je soupçonne qu'ils sont souvent confus sur la façon de résoudre chaque problème en raison de l'énorme quantité de code hérité que la «bonne» solution impliquera de toucher.
Nous allons être bientôt disponibles, et je suis sûr que les correctifs que nous appliquons seront valables pour la prochaine version. Ensuite, nous allons avoir du temps pour améliorer la base de code et refactoriser si nécessaire. Nous n'aurons pas le temps de tout réécrire. Et la majorité du code n'est pas si mal. Mais je cherche à refactoriser le code de manière à éviter complètement les problèmes de threading.
Une approche que j'envisage est la suivante. Pour chaque fonctionnalité de plate-forme importante, disposez d'un thread unique dédié sur lequel tous les événements et les rappels du réseau sont rassemblés. Similaire au filetage de cloisonnement COM dans Windows avec l'utilisation d'une boucle de message. Les opérations de blocage longues peuvent toujours être envoyées à un thread de pool de travail, mais le rappel de fin est invoqué sur le thread du composant. Les composants pourraient même partager le même thread. Ensuite, toutes les bibliothèques de classes exécutées à l'intérieur du thread peuvent être écrites sous l'hypothèse d'un monde à thread unique.
Avant de poursuivre dans cette voie, je suis également très intéressé par l'existence d'autres techniques ou modèles de conception standard pour traiter les problèmes multithreads. Et je dois souligner - quelque chose au-delà d'un livre qui décrit les bases des mutex et des sémaphores. Qu'est-ce que tu penses?
Je suis également intéressé par toute autre approche à adopter pour un processus de refactoring. Y compris l'un des éléments suivants:
Littérature ou articles sur les modèles de conception autour des fils. Quelque chose au-delà d'une introduction aux mutex et aux sémaphores. Nous n'avons pas non plus besoin d'un parallélisme massif, mais simplement de façons de concevoir un modèle objet afin de gérer correctement les événements asynchrones d'autres threads .
Façons de schématiser le filetage de divers composants, afin qu'il soit facile d'étudier et de faire évoluer des solutions. (C'est-à-dire un équivalent UML pour discuter des threads entre les objets et les classes)
Sensibiliser votre équipe de développement aux problèmes du code multithread.
Qu'est-ce que tu ferais?