C # Thread sicherer schneller (est) Zähler

147

Wie erhält man einen thread-sicheren Zähler in C # mit der bestmöglichen Leistung?

Das ist so einfach wie es nur geht:

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

Aber gibt es schnellere Alternativen?

JohnDoDo
quelle

Antworten:

108

Wie von anderen empfohlen, hat der Interlocked.Incrementeine bessere Leistung als lock(). Schauen Sie sich einfach die IL und Assembly an, wo Sie sehen werden, dass daraus Incrementeine "Bussperre" -Anweisung wird und ihre Variable direkt inkrementiert (x86) oder zu (x64) "hinzugefügt" wird.

Diese "Bus Lock" -Anweisung sperrt den Bus, um zu verhindern, dass eine andere CPU auf den Bus zugreift, während die aufrufende CPU ihren Betrieb ausführt. Schauen Sie sich jetzt lock()die IL der C # -Anweisung an. Hier sehen Sie Aufrufe an Monitor, um einen Abschnitt zu beginnen oder zu beenden.

Mit anderen Worten, die .Net- lock()Anweisung leistet viel mehr als die .Net-Anweisung Interlocked.Increment.

Wenn Sie also nur eine Variable inkrementieren möchten, ist Interlock.Incrementdies schneller. Überprüfen Sie alle Interlocked-Methoden, um die verschiedenen verfügbaren atomaren Operationen zu sehen und diejenigen zu finden, die Ihren Anforderungen entsprechen. Verwenden lock()Sie diese Option, wenn Sie komplexere Aufgaben wie mehrere miteinander verbundene Inkremente / Dekremente ausführen oder den Zugriff auf Ressourcen serialisieren möchten, die komplexer als Ganzzahlen sind.

Les
quelle
3
-1 für Implementierungsdetails. Es ist wahr, dass das Sperren viel langsamer ist als eine atomare Operation, aber dies hat nichts mit der IL zu tun. Diese Funktionsaufrufe wären viel schneller als eine atomare Operation, wenn nicht ihre Semantik, die von der IL nicht inhärent benötigt wird.
Welpe
33

Ich schlage vor, dass Sie das in .NET integrierte Inlock-Inkrement in der System.Threading-Bibliothek verwenden.

Der folgende Code erhöht eine lange Variable durch Referenz und ist vollständig threadsicher:

Interlocked.Increment(ref myNum);

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

Andrew White
quelle
1

Wie bereits erwähnt verwenden Interlocked.Increment

Codebeispiel von MS:

Das folgende Beispiel bestimmt, wie viele Zufallszahlen im Bereich von 0 bis 1.000 erforderlich sind, um 1.000 Zufallszahlen mit einem Mittelpunkt zu generieren. Um die Anzahl der Mittelpunktswerte zu verfolgen, wird eine Variable, midpointCount, auf 0 gesetzt und jedes Mal erhöht, wenn der Zufallszahlengenerator einen Mittelpunktwert zurückgibt, bis er 10.000 erreicht. Da drei Threads die Zufallszahlen generieren, wird die Increment (Int32) -Methode aufgerufen, um sicherzustellen, dass mehrere Threads midpointCount nicht gleichzeitig aktualisieren. Beachten Sie, dass eine Sperre auch zum Schutz des Zufallszahlengenerators verwendet wird und dass ein CountdownEvent-Objekt verwendet wird, um sicherzustellen, dass die Main-Methode die Ausführung nicht vor den drei Threads beendet.

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

Das folgende Beispiel ähnelt dem vorherigen, außer dass die Task-Klasse anstelle einer Thread-Prozedur verwendet wird, um 50.000 zufällige Mittelpunkt-Ganzzahlen zu generieren. In diesem Beispiel ersetzt ein Lambda-Ausdruck die Thread-Prozedur GenerateNumbers, und der Aufruf der Task.WaitAll-Methode macht das CountdownEvent-Objekt überflüssig.

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
quelle