C # Thread Safe Fast (est) compteur

147

Comment 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?

JohnDoDo
la source

Réponses:

108

Comme recommandé par d'autres, le Interlocked.Incrementaura de meilleures performances que lock(). Jetez un coup d'œil à l'IL et à l'assembly où vous verrez que cela Incrementse transforme en une instruction "bus lock" et sa variable est directement incrémentée (x86) ou "ajoutée" à (x64).

Cette instruction "bus lock" verrouille le bus pour empêcher une autre CPU d'accéder au bus pendant que la CPU appelante effectue son opération. Maintenant, jetez un œil à l' lock()IL de l'instruction C # . Ici, vous verrez des appels Monitorpour commencer ou terminer une section.

En d'autres termes, l' lock()instruction .Net fait beaucoup plus que le .Net Interlocked.Increment.

Donc, si tout ce que vous voulez faire est d'incrémenter une variable, ce Interlock.Incrementsera plus rapide. Passez en revue toutes les méthodes Interlocked pour voir les différentes opérations atomiques disponibles et pour trouver celles qui correspondent à vos besoins. À utiliser lock()lorsque vous souhaitez effectuer des opérations plus complexes, telles que plusieurs incréments / décrémentations interdépendants, ou pour sérialiser l'accès à des ressources plus complexes que des entiers.

Les
la source
3
-1 pour les détails de mise en œuvre. Il est vrai que le verrouillage est bien plus lent qu'un op atomique, mais cela n'a rien à voir avec l'IL. Ces appels de fonction seraient bien plus rapides qu'un op atomique sans leur sémantique, ce qui n'est pas intrinsèquement requis de l'IL.
Puppy
33

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émentera une variable longue par référence et est totalement thread-safe:

Interlocked.Increment(ref myNum);

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

Andrew White
la source
1

Comme déjà mentionné l'utilisation Interlocked.Increment

Exemple de code 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 garder une trace du nombre de valeurs médianes, une variable, midpointCount, est définie égale à 0 et incrémentée chaque fois que le générateur de nombres aléatoires renvoie une valeur médiane jusqu'à ce qu'elle atteigne 10 000. Étant donné que trois threads génèrent les nombres aléatoires, la méthode Increment (Int32) est appelée pour garantir 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 garantir que la méthode Main ne termine pas l'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, sauf qu'il utilise la classe Task au lieu d'une procédure de thread pour générer 50 000 entiers intermédiaires aléatoires. Dans cet exemple, une expression lambda remplace la procédure de 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

Ogglas
la source