C # Bezpieczny szybki (est) licznik wątków

147

Jaki jest sposób uzyskania licznika bezpiecznego wątku w języku C # z najlepszą możliwą wydajnością?

To jest tak proste, jak to tylko możliwe:

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

Ale czy są szybsze alternatywy?

JohnDoDo
źródło

Odpowiedzi:

108

Zgodnie z zaleceniami innych, Interlocked.Incrementbędą miały lepszą wydajność niż lock(). Wystarczy spojrzeć na IL i Assembly, gdzie zobaczysz, że Incrementzmienia się to w instrukcję „bus lock”, a jej zmienna jest bezpośrednio zwiększana (x86) lub „dodawana” do (x64).

To polecenie „bus lock” blokuje magistralę, aby uniemożliwić innemu procesorowi dostęp do magistrali, podczas gdy wywołujący procesor wykonuje swoją operację. Teraz spójrz na lock()IL instrukcji C # . Tutaj zobaczysz wezwania Monitordo rozpoczęcia lub zakończenia sekcji.

Innymi słowy, lock()instrukcja .Net robi o wiele więcej niż .Net Interlocked.Increment.

Więc jeśli wszystko, co chcesz zrobić, to inkrementować zmienną, Interlock.Incrementbędzie szybsza. Przejrzyj wszystkie metody Interlocked, aby zobaczyć różne dostępne operacje atomowe i znaleźć te, które odpowiadają Twoim potrzebom. Użyj, lock()gdy chcesz wykonywać bardziej złożone rzeczy, takie jak wiele powiązanych ze sobą przyrostów / dekrementów lub serializować dostęp do zasobów, które są bardziej złożone niż liczby całkowite.

Les
źródło
3
-1 dla szczegółów implementacji. To prawda, że ​​blokowanie jest znacznie wolniejsze niż operacja atomowa, ale nie ma to nic wspólnego z IL. Te wywołania funkcji byłyby znacznie szybsze niż atomowa operacja, gdyby nie ich semantyka, która nie jest z natury wymagana od IL.
Puppy
33

Sugeruję użycie wbudowanego inkrementacji blokady .NET w bibliotece System.Threading.

Poniższy kod zwiększy długość zmiennej przez odniesienie i jest całkowicie bezpieczny dla wątków:

Interlocked.Increment(ref myNum);

Źródło: http://msdn.microsoft.com/en-us/library/dd78zt0c.aspx

Andrew White
źródło
1

Jak już wspomniano, użyj Interlocked.Increment

Przykład kodu z MS:

Poniższy przykład określa, ile liczb losowych z zakresu od 0 do 1000 jest wymaganych do wygenerowania 1000 liczb losowych z wartością punktu środkowego. Aby śledzić liczbę wartości punktu środkowego, zmienna midpointCount jest ustawiana na 0 i zwiększana za każdym razem, gdy generator liczb losowych zwraca wartość punktu środkowego, aż osiągnie 10000. Ponieważ trzy wątki generują liczby losowe, metoda Increment (Int32) jest wywoływana w celu zapewnienia, że ​​wiele wątków nie aktualizuje współbieżnie midpointCount. Należy zauważyć, że blokada jest również używana do ochrony generatora liczb losowych i że obiekt CountdownEvent jest używany w celu zapewnienia, że ​​metoda Main nie zakończy wykonywania przed trzema wątkami.

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

Poniższy przykład jest podobny do poprzedniego, z tą różnicą, że używa klasy Task zamiast procedury wątku do generowania 50 000 losowych liczb całkowitych punktu środkowego. W tym przykładzie wyrażenie lambda zastępuje procedurę wątku GenerateNumbers, a wywołanie metody Task.WaitAll eliminuje potrzebę stosowania obiektu 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
źródło