Conditions de concurrence et interblocages

Visual Basic .NET ou Visual Basic offre la possibilité d’utiliser des threads dans des applications Visual Basic pour la première fois. Les threads introduisent des problèmes de débogage tels que les conditions de concurrence et les interblocages. Cet article explore ces deux problèmes.

Version d’origine du produit : Visual Basic, Visual Basic .NET
Numéro de la base de connaissances d’origine : 317723

Quand des conditions de concurrence se produisent

Une condition de concurrence se produit lorsque deux threads accèdent simultanément à une variable partagée. Le premier thread lit la variable et le deuxième thread lit la même valeur à partir de la variable. Ensuite, le premier thread et le deuxième thread effectuent leurs opérations sur la valeur, et ils font une course pour voir quel thread peut écrire la valeur en dernier dans la variable partagée. La valeur du thread qui écrit sa valeur en dernier est conservée, car le thread écrit sur la valeur que le thread précédent a écrite.

Détails et exemples pour une condition de concurrence

Chaque thread se voit allouer une période prédéfinie pour s’exécuter sur un processeur. Lorsque le temps alloué au thread expire, le contexte du thread est enregistré jusqu’à son prochain activation sur le processeur, et le processeur commence l’exécution du thread suivant.

Comment une commande à une ligne peut-elle provoquer une condition de concurrence ?

Examinez l’exemple suivant pour voir comment une condition de concurrence se produit. Il existe deux threads, et les deux mettent à jour une variable partagée appelée total (qui est représentée comme dword ptr ds:[031B49DCh] dans le code de l’assembly).

  • Thread 1

    Total = Total + val1
    
  • Thread 2

    Total = Total - val2
    

Code d’assembly (avec numéros de ligne) de la compilation du code Visual Basic précédent :

  • Thread 1

    1. mov eax,dword ptr ds:[031B49DCh]
    2. add eax,edi
    3. jno 00000033
    4. xor ecx,ecx
    5. call 7611097F
    6. mov dword ptr ds:[031B49DCh],eax
    
  • Thread 2

    1. mov eax,dword ptr ds:[031B49DCh]
    2. sub eax,edi
    3. jno 00000033
    4. xor ecx,ecx
    5. call 76110BE7
    6. mov dword ptr ds:[031B49DCh],eax
    

En examinant le code d’assembly, vous pouvez voir le nombre d’opérations effectuées par le processeur au niveau inférieur pour exécuter un calcul d’addition simple. Un thread peut être en mesure d’exécuter tout ou partie de son code d’assembly pendant son temps sur le processeur. Regardez maintenant comment une condition de concurrence se produit à partir de ce code.

Total est 100, val1 50 et val2 15. Le thread 1 a la possibilité de s’exécuter, mais effectue uniquement les étapes 1 à 3. Cela signifie que le thread 1 a lu la variable et terminé l’ajout. Le thread 1 n’attend plus qu’à écrire sa nouvelle valeur de 150. Une fois le thread 1 arrêté, le thread 2 s’exécute complètement. Cela signifie qu’il a écrit la valeur qu’il a calculée (85) dans la variable Total. Enfin, le thread 1 reprend le contrôle et termine l’exécution. Il écrit sa valeur (150). Par conséquent, lorsque le thread 1 est terminé, la valeur de Total est désormais 150 au lieu de 85.

Vous pouvez voir comment cela peut être un problème majeur. S’il s’agit d’un programme bancaire, le client aurait de l’argent sur son compte qui ne devrait pas être présent.

Cette erreur est aléatoire, car il est possible que le thread 1 termine son exécution avant l’expiration du processeur, puis que le thread 2 puisse commencer son exécution. Si ces événements se produisent, le problème ne se produit pas. L’exécution du thread n’est pas déterministe. Par conséquent, vous ne pouvez pas contrôler l’heure ou l’ordre d’exécution. Notez également que les threads peuvent s’exécuter différemment en mode runtime et en mode débogage. En outre, vous pouvez voir que si vous exécutez chaque thread en série, l’erreur ne se produit pas. Ce caractère aléatoire rend ces erreurs beaucoup plus difficiles à localiser et à déboguer.

Pour éviter que les conditions de concurrence ne se produisent, vous pouvez verrouiller les variables partagées, afin qu’un seul thread à la fois ait accès à la variable partagée. Effectuez cette opération avec parcimonie, car si une variable est verrouillée dans le thread 1 et que le thread 2 a également besoin de la variable, l’exécution du thread 2 s’arrête tandis que le thread 2 attend que le thread 1 libère la variable. (Pour plus d’informations, consultez SyncLock la section Références de cet article.)

Symptômes d’une condition de race

Le symptôme le plus courant d’une condition de concurrence est des valeurs imprévisibles de variables partagées entre plusieurs threads. Cela résulte de l’imprévisibilité de l’ordre dans lequel les threads s’exécutent. Parfois, un thread gagne, et parfois l’autre l’emporte. À d’autres moments, l’exécution fonctionne correctement. En outre, si chaque thread est exécuté séparément, la valeur de variable se comporte correctement.

Quand des interblocages se produisent

Un interblocage se produit lorsque deux threads verrouillent chacun une variable différente en même temps, puis tentent de verrouiller la variable que l’autre thread a déjà verrouillée. Par conséquent, chaque thread cesse de s’exécuter et attend que l’autre thread libère la variable. Étant donné que chaque thread contient la variable souhaitée par l’autre thread, rien ne se produit et les threads restent bloqués.

Détails et exemples pour les interblocages

Le code suivant a deux objets, LeftVal et RightVal:

  • Thread 1

    SyncLock LeftVal
        SyncLock RightVal
            'Perform operations on LeftVal and RightVal that require read and write.
        End SyncLock
    End SyncLock
    
  • Thread 2

    SyncLock RightVal
        SyncLock LeftVal
            'Perform operations on RightVal and LeftVal that require read and write.
        End SyncLock
    End SyncLock
    

Un interblocage se produit lorsque le thread 1 est autorisé à verrouiller LeftVal. Le processeur arrête l’exécution du thread 1 et commence l’exécution du thread 2. Le thread 2 verrouille RightVal , puis tente de verrouiller LeftVal. Étant donné que LeftVal est verrouillé, le thread 2 s’arrête et attend qu’il LeftVal soit libéré. Étant donné que le thread 2 est arrêté, le thread 1 est autorisé à continuer à s’exécuter. Le thread 1 tente de verrouiller RightVal mais ne peut pas, car le thread 2 l’a verrouillé. Par conséquent, le thread 1 commence à attendre que RightVal soit disponible. Chaque thread attend l’autre thread, car chaque thread a verrouillé la variable que l’autre thread attend, et aucun des threads ne déverrouille la variable qu’il contient.

Un interblocage ne se produit pas toujours. Si le thread 1 exécute les deux verrous avant que le processeur ne l’arrête, le thread 1 peut effectuer ses opérations, puis déverrouiller la variable partagée. Une fois que le thread 1 a déverrouillé la variable, le thread 2 peut poursuivre son exécution, comme prévu.

Cette erreur semble évidente lorsque ces extraits de code sont placés côte à côte, mais dans la pratique, le code peut apparaître dans des modules ou des zones distincts de votre code. Il s’agit d’une erreur difficile à rechercher, car, à partir de ce même code, l’exécution correcte et l’exécution incorrecte peuvent se produire.

Symptômes des interblocages

Un symptôme courant d’interblocage est que le programme ou le groupe de threads cesse de répondre. C’est également ce que l’on appelle un blocage. Au moins deux threads attendent une variable que l’autre thread a verrouillée. Les threads ne continuent pas, car aucun des threads ne libère sa variable tant qu’il n’obtient pas l’autre variable. L’ensemble du programme peut se bloquer si le programme attend l’un de ces threads ou les deux pour terminer l’exécution.

Qu’est-ce qu’un thread ?

Les processus sont utilisés pour séparer les différentes applications qui s’exécutent à un moment spécifié sur un seul ordinateur. Le système d’exploitation n’exécute pas de processus, contrairement aux threads. Un thread est une unité d’exécution. Le système d’exploitation alloue du temps processeur à un thread pour l’exécution des tâches du thread. Un seul processus peut contenir plusieurs threads d’exécution. Chaque thread conserve ses propres gestionnaires d’exceptions, ses priorités de planification et un ensemble de structures que le système d’exploitation utilise pour enregistrer le contexte du thread si le thread ne peut pas terminer son exécution pendant le temps où il a été affecté au processeur. Le contexte est conservé jusqu’à la prochaine fois que le thread reçoit le temps processeur. Le contexte inclut toutes les informations dont le thread a besoin pour poursuivre son exécution en toute transparence. Ces informations incluent l’ensemble des registres de processeur du thread et la pile des appels à l’intérieur de l’espace d’adressage du processus hôte.

References

Pour plus d’informations, recherchez les mots clés suivants dans l’aide de Visual Studio :

  • SyncLock. Autorise le verrouillage d’un objet. Si un autre thread tente de verrouiller ce même objet, il est bloqué jusqu’à ce que le premier thread soit libéré. Utilisez SyncLock soigneusement, car des problèmes peuvent résulter d’une mauvaise utilisation de SyncLock. Par exemple, cette commande peut empêcher des conditions de concurrence, mais provoquer des interblocages.

  • InterLocked. Autorise un ensemble sélectionné d’opérations thread-safe sur des variables numériques de base.

Pour plus d’informations, consultez Threads et threading.