176 votes

C# Compteur rapide (est) de threads sécurisés

Quel est le moyen d'obtenir un compteur thread safe en C# avec les meilleures performances possibles ?

C'est aussi simple que cela :

public static long GetNextValue()
{
    long result;
    lock (LOCK)
    {
        result = COUNTER++;
    }
    return result;
}

Mais existe-t-il des alternatives plus rapides ?

292voto

Austin Salonen Points 28057

Ce serait plus simple :

return Interlocked.Increment(ref COUNTER);

MSDN Interlocked.Increment

0 votes

C'est rapide, mais pas encore assez dans les scénarios de performances extrêmes. Néanmoins, ce n'est pas comme si nous avions de meilleures alternatives.

115voto

Les Points 2432

Comme recommandé par d'autres, le Interlocked.Increment aura de meilleures performances que lock() . Il suffit de jeter un coup d'œil à l'IL et à l'Assemblée où vous verrez que Increment se transforme en une instruction de "verrouillage de bus" et sa variable est directement incrémentée (x86) ou "ajoutée" (x64).

Cette instruction "bus lock" verrouille le bus afin d'empêcher une autre unité centrale d'accéder au bus pendant que l'unité centrale appelante effectue son opération. Maintenant, regardez l'instruction C# lock() déclaration de l'IL. Vous y verrez des appels à Monitor afin de commencer ou de terminer une section.

En d'autres termes, .Net lock() fait beaucoup plus que la déclaration .Net. Interlocked.Increment .

Donc, si tout ce que vous voulez faire est d'incrémenter une variable, Interlock.Increment sera plus rapide. Passez en revue toutes les méthodes d'imbrication pour voir les différentes opérations atomiques disponibles et trouver celles qui répondent à vos besoins. Utilisez lock() lorsque vous voulez faire des choses plus complexes, comme de multiples incréments/décréments interdépendants, ou pour sérialiser l'accès à des ressources qui sont plus complexes que des entiers.

3 votes

-1 pour les détails de mise en œuvre. Il est vrai que le verrouillage est beaucoup plus lent qu'une opération atomique, mais cela n'a rien à voir avec l'IL. Ces appels de fonction seraient bien plus rapides qu'une opération atomique si ce n'était de leur sémantique, qui n'est pas intrinsèquement requise par l'IL.

36voto

Andrew White Points 506

Je vous suggère d'utiliser l'incrément de verrouillage intégré de .NET dans la bibliothèque System.Threading.

Le code suivant incrémente une variable longue par référence et est complètement thread safe :

Interlocked.Increment(ref myNum);

Source : http://msdn.microsoft.com/en-us/library/dd78zt0c.aspx

18voto

fsimonazzi Points 1841

Essayez avec Interlocked.Increment

0voto

Ogglas Points 1

Comme déjà mentionné, utilisez Interlocked.Increment

Exemple de code provenant de MS :

L'exemple suivant détermine combien de nombres aléatoires compris entre 0 et 1 000 sont nécessaires pour générer 1 000 nombres aléatoires avec une valeur médiane. Pour suivre le nombre de valeurs intermédiaires, une variable, midpointCount, est définie à 0 et est incrémentée chaque fois que le générateur de nombres aléatoires renvoie une valeur intermédiaire jusqu'à ce qu'elle atteigne 10 000. Comme trois threads génèrent les nombres aléatoires, la méthode Increment(Int32) est appelée pour s'assurer que plusieurs threads ne mettent pas à jour midpointCount simultanément. Notez qu'un verrou est également utilisé pour protéger le générateur de nombres aléatoires et qu'un objet CountdownEvent est utilisé pour s'assurer que la méthode Main ne termine pas son exécution avant les trois threads.

using System;
using System.Threading;

public class Example
{
   const int LOWERBOUND = 0;
   const int UPPERBOUND = 1001;

   static Object lockObj = new Object();
   static Random rnd = new Random();
   static CountdownEvent cte;

   static int totalCount = 0;
   static int totalMidpoint = 0;
   static int midpointCount = 0;

   public static void Main()
   {
      cte = new CountdownEvent(1);
      // Start three threads. 
      for (int ctr = 0; ctr <= 2; ctr++) {
         cte.AddCount();
         Thread th = new Thread(GenerateNumbers);
         th.Name = "Thread" + ctr.ToString();
         th.Start();
      }
      cte.Signal();
      cte.Wait();
      Console.WriteLine();
      Console.WriteLine("Total midpoint values:  {0,10:N0} ({1:P3})",
                        totalMidpoint, totalMidpoint/((double)totalCount));
      Console.WriteLine("Total number of values: {0,10:N0}", 
                        totalCount);                  
   }

   private static void GenerateNumbers()
   {
      int midpoint = (UPPERBOUND - LOWERBOUND) / 2;
      int value = 0;
      int total = 0;
      int midpt = 0;

      do {
         lock (lockObj) {
            value = rnd.Next(LOWERBOUND, UPPERBOUND);
         }
         if (value == midpoint) { 
            Interlocked.Increment(ref midpointCount);
            midpt++;
         }
         total++;    
      } while (midpointCount < 10000);

      Interlocked.Add(ref totalCount, total);
      Interlocked.Add(ref totalMidpoint, midpt);

      string s = String.Format("Thread {0}:\n", Thread.CurrentThread.Name) +
                 String.Format("   Random Numbers: {0:N0}\n", total) + 
                 String.Format("   Midpoint values: {0:N0} ({1:P3})", midpt, 
                               ((double) midpt)/total);
      Console.WriteLine(s);
      cte.Signal();
   }
}
// The example displays output like the following:
//       Thread Thread2:
//          Random Numbers: 2,776,674
//          Midpoint values: 2,773 (0.100 %)
//       Thread Thread1:
//          Random Numbers: 4,876,100
//          Midpoint values: 4,873 (0.100 %)
//       Thread Thread0:
//          Random Numbers: 2,312,310
//          Midpoint values: 2,354 (0.102 %)
//       
//       Total midpoint values:      10,000 (0.100 %)
//       Total number of values:  9,965,084

L'exemple suivant est similaire au précédent, à ceci près qu'il utilise la classe Task au lieu d'une procédure thread pour générer 50 000 entiers aléatoires de point milieu. Dans cet exemple, une expression lambda remplace la procédure thread GenerateNumbers, et l'appel à la méthode Task.WaitAll élimine le besoin de l'objet CountdownEvent.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

public class Example
{
   const int LOWERBOUND = 0;
   const int UPPERBOUND = 1001;

   static Object lockObj = new Object();
   static Random rnd = new Random();

   static int totalCount = 0;
   static int totalMidpoint = 0;
   static int midpointCount = 0;

   public static void Main()
   {
      List<Task> tasks = new List<Task>();
      // Start three tasks. 
      for (int ctr = 0; ctr <= 2; ctr++) 
         tasks.Add(Task.Run( () => { int midpoint = (UPPERBOUND - LOWERBOUND) / 2;
                                     int value = 0;
                                     int total = 0;
                                     int midpt = 0;

                                     do {
                                        lock (lockObj) {
                                           value = rnd.Next(LOWERBOUND, UPPERBOUND);
                                        }
                                        if (value == midpoint) { 
                                           Interlocked.Increment(ref midpointCount);
                                           midpt++;
                                        }
                                        total++;    
                                     } while (midpointCount < 50000);

                                     Interlocked.Add(ref totalCount, total);
                                     Interlocked.Add(ref totalMidpoint, midpt);

                                     string s = String.Format("Task {0}:\n", Task.CurrentId) +
                                                String.Format("   Random Numbers: {0:N0}\n", total) + 
                                                String.Format("   Midpoint values: {0:N0} ({1:P3})", midpt, 
                                                              ((double) midpt)/total);
                                     Console.WriteLine(s); } ));

      Task.WaitAll(tasks.ToArray());
      Console.WriteLine();
      Console.WriteLine("Total midpoint values:  {0,10:N0} ({1:P3})",
                        totalMidpoint, totalMidpoint/((double)totalCount));
      Console.WriteLine("Total number of values: {0,10:N0}", 
                        totalCount);                  
   }
}
// The example displays output like the following:
//       Task 3:
//          Random Numbers: 10,855,250
//          Midpoint values: 10,823 (0.100 %)
//       Task 1:
//          Random Numbers: 15,243,703
//          Midpoint values: 15,110 (0.099 %)
//       Task 2:
//          Random Numbers: 24,107,425
//          Midpoint values: 24,067 (0.100 %)
//       
//       Total midpoint values:      50,000 (0.100 %)
//       Total number of values: 50,206,378

https://docs.microsoft.com/en-us/dotnet/api/system.threading.interlocked.increment?view=netcore-3.0

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