Contatore veloce (est) sicuro thread # C


147

Qual è il modo per ottenere un contatore thread-safe in C # con le migliori prestazioni possibili?

Questo è semplice come si ottiene:

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

Ma ci sono alternative più veloci?

Risposte:



108

Come raccomandato da altri, Interlocked.Incrementavrà prestazioni migliori rispetto a lock(). Dai un'occhiata a IL e Assembly in cui vedrai che Incrementsi trasforma in un'istruzione "blocco bus" e la sua variabile viene incrementata direttamente (x86) o "aggiunta" a (x64).

Questa istruzione "blocco bus" blocca il bus per impedire ad un'altra CPU di accedere al bus mentre la CPU chiamante fa il suo funzionamento. Ora dai un'occhiata lock()all'IL dell'istruzione C # . Qui vedrai le chiamate Monitorper iniziare o terminare una sezione.

In altre parole, l' lock()istruzione .Net sta facendo molto di più di .Net Interlocked.Increment.

Quindi, se tutto ciò che vuoi fare è incrementare una variabile, Interlock.Incrementsarà più veloce. Esamina tutti i metodi interbloccati per vedere le varie operazioni atomiche disponibili e per trovare quelle che soddisfano le tue esigenze. Utilizzare lock()quando si desidera eseguire operazioni più complesse come multipli incrementi / decrementi correlati o per serializzare l'accesso a risorse più complesse degli interi.


3
-1 per i dettagli di implementazione. È vero che il bloccaggio è molto più lento di un'operazione atomica, ma questo non ha nulla a che fare con l'IL. Quelle chiamate di funzione sarebbero molto più veloci di un'operazione atomica se non fosse per la loro semantica, cosa che non è intrinsecamente richiesta dall'IL.
Cucciolo



1

Come già accennato usare Interlocked.Increment

Esempio di codice da MS:

L'esempio seguente determina quanti numeri casuali che vanno da 0 a 1.000 sono necessari per generare 1.000 numeri casuali con un valore di punto medio. Per tenere traccia del numero di valori del punto medio, una variabile, punto intermedio, viene impostata uguale a 0 e incrementata ogni volta che il generatore di numeri casuali restituisce un valore del punto medio fino a raggiungere 10.000. Poiché tre thread generano i numeri casuali, viene chiamato il metodo Increment (Int32) per garantire che più thread non aggiornino midpointCount contemporaneamente. Si noti che un blocco viene utilizzato anche per proteggere il generatore di numeri casuali e che un oggetto CountdownEvent viene utilizzato per garantire che il metodo Main non completi l'esecuzione prima dei tre thread.

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'esempio seguente è simile al precedente, tranne per il fatto che utilizza la classe Task anziché una procedura di thread per generare 50.000 numeri interi casuali del punto medio. In questo esempio, un'espressione lambda sostituisce la procedura del thread GenerateNumbers e la chiamata al metodo Task.WaitAll elimina la necessità dell'oggetto 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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.