Résoudre un problème d’ordre dans l’acquisition de verrous en programmation parallèle

La programmation parallèle est souvent considérée comme compliquée et particulièrement après quelques expériences éprouvantes à tenter de comprendre un problème de synchronisation. Par exemple, voici l’exemple classique d’un transfert d’argent entre deux comptes, source et destination, dont l’accès a été protégé par des sections critiques.

public static bool Transfert(Account source, Account destination, decimal amount)

{

    bool success;

 

    lock (source)

    {

        lock (destination)

        {

            success = source.CanWithdraw(amount);

 

            if (success)

            {

                source.Deposit(-amount);

                destination.Deposit(amount);

            }                                          

        }

    }

 

    return success;

}

 

Le souci avec ce code est que nous supposons que l’ordre de l’enchaînement des verrous est garanti, ce qui est complètement faux. En effet, nous pouvons appeler plusieurs fois la méthode Transfert depuis des threads différents en croisant les comptes, le tout dans une boucle pour être certains que nous tomberons sur un problème rapidement :

 Task.Factory.StartNew(() =>
 {
     Bank.Transfert(accountA, accountB, 20);
 });
  
 Task.Factory.StartNew(() =>
 {
     Bank.Transfert(accountB, accountA, 10);
 });

 

Si vous lancez ce type d’application sous le debugger de Visual Studio 2010, et que vous placez en pause votre programme, vous observez dans la fenêtre « Parallel Tasks » une situation similaire.

clip_image002[4]

Le symbole « Sens interdit » indique une situation de DeadLock, où plus aucune tâche ne s’exécute, car elles sont toutes en attentes mutuelles. Notre problème est fréquent en programmation parallèle dès que nous utilisons des verrous. Si fonctionnellement vous avez besoin de considérer un ordre d’acquisition de verrous, vous devez vous poser la question : comment garantir l'ordre d'acquisition des verrous dans un contexte multitâches ? Ce n’est pas toujours simple comme mon exemple, mais le principe reste le même. Pour corriger notre problème et rendre notre méthode Transfert indépendante des paramètres engagés dans une synchronisation ordonnée, voici une nouvelle version :

 public static bool Transfert(Account source, Account destination, decimal amount)
 {
     bool success;
 var hash1 = source.GetHashCode(); 
 var hash2 = destination.GetHashCode(); 
 
 var minlock = (hash1 < hash2) ? source.AccountLock : destination.AccountLock; 
 var maxlock = (hash1 > hash2) ? source.AccountLock : destination.AccountLock; 
  
     lock (minlock)
     {
         lock (maxlock)
         {
             success = source.CanWithdraw(amount);
  
             if (success)
             {
                 source.Deposit(-amount);
                 destination.Deposit(amount);
             }                    
  
         }
     }
     return success;
 }

 

En introduisant un moyen de discriminer l’ordre d’appel des verrous sur la base des objets engagés dans la synchronisation (ici clef de hash des objets respectifs), nous garantissons le bon fonctionnement de notre méthode indépendamment de l’ordre des paramètres.

A bientôt,

Bruno

Boucard.bruno@free.fr