55 votes

Comment puis-je savoir si cette méthode C# est thread safe ?

Je travaille à la création d'une fonction de rappel pour un événement ASP.NET de suppression d'un élément du cache.

La documentation indique que je dois appeler une méthode sur un objet ou des appels dont je sais qu'ils existeront (seront dans le champ d'application), comme une méthode statique, mais elle précise que je dois m'assurer que la méthode statique est thread safe.

Partie 1 : Quels sont les exemples de choses que je pourrais faire pour rendre le système non threadé sûr ?

Partie 2 : Est-ce que cela signifie que si j'ai

static int addOne(int someNumber){
    int foo = someNumber;
    return foo +1; 
}

et que j'appelle Class.addOne(5) ; et Class.addOne(6) ; en même temps, est-ce que je pourrais avoir 6 ou 7 renvoyés selon l'invocation qui définit foo en premier ? (c'est-à-dire une condition de course)

59voto

Cybis Points 5062

Ce addOne est effectivement thread safe car elle n'accède à aucune donnée qui pourrait être accessible par un autre thread. Les variables locales ne peuvent pas être partagées entre les threads car chaque thread a sa propre pile. Vous devez cependant vous assurer que les paramètres de la fonction sont des types de valeur et non des types de référence.

static void MyFunction(int x) { ... } // thread safe. The int is copied onto the local stack.

static void MyFunction(Object o) { ... } // Not thread safe. Since o is a reference type, it might be shared among multiple threads.

0 votes

Que se passe-t-il si x est un type de valeur mais est transmis par référence ? static void MyFunction(ref int x) { ... } . Sera-t-il toujours à l'abri des fils ?

41voto

Jon Skeet Points 692016

Non, addOne est thread-safe ici - il utilise uniquement des variables locales. Voici un exemple qui ne serait pas être à l'abri des fils :

 class BadCounter
 {
       private static int counter;

       public static int Increment()
       {
             int temp = counter;
             temp++;
             counter = temp;
             return counter;
       }
 }

Ici, deux threads pourraient appeler Increment en même temps, et ne s'incrémenter qu'une seule fois. (En utilisant return ++counter; serait tout aussi mauvais, d'ailleurs - ce qui précède est une version plus explicite de la même chose. Je l'ai développée pour qu'elle soit plus manifestement mauvaise).

Les détails de ce qui est ou n'est pas thread-safe peuvent être assez délicats, mais en général, si vous ne modifiez aucun état (autre que ce qui vous a été passé, de toute façon - un peu de zone grise ici), alors c'est généralement OK.

1 votes

Si on vous passe une valeur ou un type immuable comme une chaîne de caractères, vous devriez pouvoir le muter en toute sécurité, correct ? Si on vous transmet un objet de référence, alors c'est là que ça devient gris ?

1 votes

@Josh : En général, si vous passez quelque chose à la méthode, alors la modification de cet élément est thread safe car chaque appelant passe son propre élément. La clé dans l'exemple de Jon est que chaque thread modifie le même membre statique "counter".

1 votes

S'il est immuable, vous ne pourrez pas le muter :) L'appel à une méthode qui crée simplement une nouvelle instance (comme le fait string.Replace) est sans danger. (Une chaîne de caractères est toujours un type de référence, soit dit en passant.) La zone grise est si on vous passe une référence à un objet mutable - les méthodes d'instance sur celui-ci (suite)

23voto

Rob Parker Points 1674

Les problèmes de threads (qui m'ont également préoccupé ces derniers temps) découlent de l'utilisation de plusieurs cœurs de processeurs avec des caches séparés, ainsi que des conditions de course de base pour le remplacement des threads. Si les caches de cœurs distincts accèdent au même emplacement de mémoire, ils n'auront généralement aucune idée de l'autre et pourront suivre séparément l'état de cet emplacement de données sans qu'il ne retourne à la mémoire principale (ou même à un cache synchronisé partagé par tous les cœurs à L2 ou L3, par exemple), pour des raisons de performances du processeur. Ainsi, même les astuces de verrouillage de l'ordre d'exécution peuvent ne pas être fiables dans les environnements multithreads.

Comme vous le savez peut-être, le principal outil permettant de corriger ce problème est le verrou, qui fournit un mécanisme d'accès exclusif (entre les contentions pour le même verrou) et gère la synchronisation du cache sous-jacent de sorte que les accès au même emplacement mémoire par diverses sections de code protégées par un verrou soient correctement sérialisés. Vous pouvez toujours avoir des conditions de course entre qui obtient le verrou quand et dans quel ordre, mais c'est généralement beaucoup plus simple à gérer lorsque vous pouvez garantir que l'exécution d'une section verrouillée est atomique (dans le contexte de ce verrou).

Vous pouvez obtenir un verrou sur une instance de n'importe quel type de référence (par exemple, hérite de Object, pas des types de valeur comme int ou enums, et pas null), mais il est très important de comprendre que le verrou sur un objet n'a aucun effet inhérent sur les accès à cet objet, il interagit seulement avec d'autres tentatives d'obtenir le verrou sur le même objet. C'est à la classe de protéger l'accès à ses variables membres en utilisant un schéma de verrouillage approprié. Parfois, les instances peuvent protéger les accès multithread à leurs propres membres en se verrouillant elles-mêmes (par ex. lock (this) { ... } ), mais cela n'est généralement pas nécessaire car les instances sont généralement détenues par un seul propriétaire et il n'est pas nécessaire de garantir un accès threadsafe à l'instance.

Plus souvent, une classe crée un verrou privé (ex. private readonly object m_Lock = new Object(); pour des verrous séparés dans chaque instance afin de protéger l'accès aux membres de cette instance, ou private static readonly object s_Lock = new Object(); pour un verrouillage central afin de protéger l'accès aux membres statiques de la classe). Josh a un exemple de code plus spécifique de l'utilisation d'un verrou. Vous devez ensuite coder la classe pour utiliser le verrou de manière appropriée. Dans des cas plus complexes, vous pouvez même créer des verrous séparés pour différents groupes de membres, afin de réduire les conflits pour différents types de ressources qui ne sont pas utilisées ensemble.

Ainsi, pour en revenir à votre question initiale, une méthode qui n'accède qu'à ses propres variables et paramètres locaux serait thread-safe, car ceux-ci existent dans leurs propres emplacements mémoire sur la pile spécifique au thread actuel, et ne peuvent être accédés ailleurs - à moins que vous ne partagiez ces instances de paramètres entre les threads avant de les transmettre.

Une méthode non statique qui n'accède qu'aux membres propres de l'instance (pas de membres statiques) - et bien sûr aux paramètres et aux variables locales - n'aurait pas besoin d'utiliser des verrous dans le contexte où cette instance est utilisée par un seul propriétaire (il n'est pas nécessaire qu'elle soit thread-safe), mais si les instances étaient destinées à être partagées et que l'on voulait garantir un accès thread-safe, alors l'instance devrait protéger l'accès à ses variables membres avec un ou plusieurs verrous spécifiques à cette instance (le verrouillage sur l'instance elle-même étant une option) - plutôt que de laisser à l'appelant le soin d'implémenter ses propres verrous autour de l'instance lorsqu'il partage quelque chose qui n'est pas destiné à être partagé de manière thread-safe.

L'accès aux membres en lecture seule (statiques ou non) qui ne sont jamais manipulés est généralement sûr, mais si l'instance qu'il contient n'est pas elle-même thread-safe ou si vous devez garantir l'atomicité à travers de multiples manipulations de celle-ci, alors vous devrez peut-être protéger tous les accès à celle-ci avec votre propre schéma de verrouillage. C'est un cas où il pourrait être pratique que l'instance utilise le verrouillage sur elle-même, parce que vous pourriez simplement obtenir un verrou sur l'instance à travers de multiples accès à elle pour l'atomicité, mais vous n'auriez pas besoin de le faire pour les accès uniques à elle si elle utilise un verrou sur elle-même pour rendre ces accès individuellement thread-safe. (Si ce n'est pas votre classe, vous devrez savoir si elle se verrouille sur elle-même ou si elle utilise un verrou privé auquel vous ne pouvez pas accéder en externe).

Enfin, il y a l'accès aux membres statiques changeants (modifiés par la méthode donnée ou par d'autres) à partir d'une instance - et bien sûr les méthodes statiques qui accèdent à ces membres statiques et peuvent être appelées par n'importe qui, n'importe où, n'importe quand - qui ont le plus besoin d'utiliser un verrouillage responsable, sans quoi elles ne sont certainement pas thread-safe et sont susceptibles de provoquer des bogues imprévisibles.

Lorsqu'il s'agit de classes du cadre .NET, Microsoft documente dans MSDN si un appel API donné est thread-safe (par exemple, les méthodes statiques des types de collection génériques fournis comme List<T> sont rendues thread-safe alors que les méthodes d'instance peuvent ne pas l'être - mais vérifiez spécifiquement pour être sûr). La grande majorité du temps (et à moins qu'il ne soit spécifiquement indiqué qu'il est "thread-safe"), il n'est pas "thread-safe" en interne, il est donc de votre responsabilité de l'utiliser de manière sûre. Et même lorsque des opérations individuelles sont implémentées en interne de manière thread-safe, vous devez toujours vous soucier des accès partagés et superposés par votre code s'il fait quelque chose de plus complexe qui doit être atomique.

L'itération sur une collection (par exemple, avec la fonction foreach ). Même si chaque accès à la collection obtient un état stable, il n'y a aucune garantie inhérente qu'il ne changera pas entre ces accès (si quelqu'un d'autre peut y accéder). Lorsque la collection est conservée localement, il n'y a généralement pas de problème, mais une collection qui pourrait être modifiée (par un autre thread ou pendant l'exécution de votre boucle !) pourrait produire des résultats incohérents. Une façon simple de résoudre ce problème est d'utiliser une opération atomique thread-safe (à l'intérieur de votre schéma de verrouillage de protection) pour faire une copie temporaire de la collection ( MyType[] mySnapshot = myCollection.ToArray(); ) et ensuite itérer sur cette copie locale de l'instantané en dehors du verrou. Dans de nombreux cas, cela évite d'avoir à maintenir un verrou tout le temps, mais selon ce que vous faites dans l'itération, cela peut ne pas être suffisant et vous devez simplement vous protéger contre les changements tout le temps (ou vous pouvez déjà l'avoir à l'intérieur d'une section verrouillée se protégeant contre l'accès pour changer la collection avec d'autres choses, donc c'est couvert).

Ainsi, la conception de threads sécurisés est un peu un art, et savoir exactement où et comment obtenir des verrous pour protéger les choses dépend beaucoup de la conception globale et de l'utilisation de votre ou vos classes. Il peut être facile de devenir paranoïaque et de penser qu'il faut mettre des verrous partout pour tout, mais il s'agit en fait de trouver la bonne couche de protection.

15 votes

Je ne comprends pas un mot de ce que vous venez de dire mais +1 pour l'effort.

6 votes

Vous devriez ouvrir un blog, comme ça vous n'auriez pas besoin de l'excuse des questions pour écrire des articles !

8voto

JoshBerke Points 34238

Votre méthode est bonne puisqu'elle n'utilise que des variables locales, modifions un peu votre méthode :

static int foo;

static int addOne(int someNumber)
{
  foo=someNumber; 
  return foo++;
}

Il ne s'agit pas d'une méthode thread safe car nous touchons à des données statiques. Il faudrait alors la modifier pour qu'elle le soit :

static int foo;
static object addOneLocker=new object();
static int addOne(int someNumber)
{
  int myCalc;
  lock(addOneLocker)
  {
     foo=someNumber; 
     myCalc= foo++;
  }
  return myCalc;
}

Je pense que c'est un échantillon stupide que je viens de faire parce que si je le lis correctement, il n'y a plus d'intérêt à faire du foo mais bon, c'est un échantillon.

3voto

TheSmurf Points 10872

Ce ne serait une condition de course que si elle modifiait une variable externe à la fonction. Votre exemple ne fait pas cela.

C'est en gros ce que vous devez rechercher. Thread safe signifie que la fonction soit :

  1. ne modifie pas les données externes, ou
  2. L'accès aux données externes est correctement synchronisé afin qu'une seule fonction puisse y accéder à tout moment.

Les données externes peuvent être quelque chose de stocké (base de données/fichier), ou quelque chose d'interne à l'application (une variable, une instance d'une classe, etc.) : en gros, tout ce qui est déclaré n'importe où dans le monde et qui est en dehors de la portée de la fonction.

Un exemple trivial d'une version non threadée de votre fonction serait le suivant :

private int myVar = 0;

private void addOne(int someNumber)
{
   myVar += someNumber;
}

Si vous appelez cette fonction à partir de deux threads différents sans synchronisation, l'interrogation de la valeur de myVar sera différente selon que l'interrogation a lieu après que tous les appels à addOne sont terminés, ou que l'interrogation a lieu entre les deux appels, ou encore que l'interrogation a lieu avant l'un ou l'autre des appels.

0 votes

Je ne pense pas que cela compilera, car les membres statiques ne peuvent pas accéder aux membres d'instance. (addOne ne peut pas accéder à myVar).

0 votes

Vous avez raison, ça ne sera pas le cas. Ça fait trop longtemps que je ne me souviens pas pourquoi c'est comme ça, alors je l'ai corrigé.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X