C # hilo seguro rápido (est) contador

147

¿Cuál es la forma de obtener un contador seguro para subprocesos en C # con el mejor rendimiento posible?

Esto es tan simple como se pone:

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

¿Pero hay alternativas más rápidas?

JohnDoDo
fuente

Respuestas:

255

Esto sería más simple:

return Interlocked.Increment(ref COUNTER);

MSDN enclavado.

Austin Salonen
fuente
108

Según lo recomendado por otros, Interlocked.Incrementtendrá un mejor rendimiento que lock(). Solo eche un vistazo a IL y Assembly, donde verá que se Incrementconvierte en una declaración de "bloqueo de bus" y su variable se incrementa directamente (x86) o "se agrega" a (x64).

Esta declaración de "bloqueo de bus" bloquea el bus para evitar que otra CPU acceda al bus mientras la CPU que llama realiza su operación. Ahora, eche un vistazo a la lock()IL de la declaración de C # . Aquí verá llamadas Monitorpara comenzar o finalizar una sección.

En otras palabras, la lock()declaración .Net está haciendo mucho más que la .Net Interlocked.Increment.

Entonces, si todo lo que quieres hacer es incrementar una variable, Interlock.Incrementserá más rápido. Revise todos los métodos enclavados para ver las diversas operaciones atómicas disponibles y para encontrar las que se adapten a sus necesidades. Úselo lock()cuando desee hacer cosas más complejas, como múltiples incrementos / decrementos interrelacionados, o para serializar el acceso a recursos que son más complejos que los enteros.

Les
fuente
3
-1 para detalles de implementación. Es cierto que el bloqueo es mucho más lento que una operación atómica, pero esto no tiene nada que ver con la IL. Esas llamadas a funciones serían mucho más rápidas que una operación atómica si no fuera por su semántica, que no es inherentemente requerida por la IL.
Cachorro
33

Le sugiero que use el incremento de enclavamiento integrado de .NET en la biblioteca System.Threading.

El siguiente código incrementará una variable larga por referencia y es completamente seguro para subprocesos:

Interlocked.Increment(ref myNum);

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

Andrew White
fuente
1

Como ya se mencionó uso Interlocked.Increment

Ejemplo de código de MS:

El siguiente ejemplo determina cuántos números aleatorios que van de 0 a 1,000 son necesarios para generar 1,000 números aleatorios con un valor de punto medio. Para realizar un seguimiento del número de valores de punto medio, una variable, midpointCount, se establece igual a 0 y se incrementa cada vez que el generador de números aleatorios devuelve un valor de punto medio hasta llegar a 10,000. Debido a que tres subprocesos generan los números aleatorios, se llama al método Incremento (Int32) para garantizar que varios subprocesos no actualicen midpointCount simultáneamente. Tenga en cuenta que también se usa un bloqueo para proteger el generador de números aleatorios, y que se usa un objeto CountdownEvent para garantizar que el método Main no termine la ejecución antes de los tres subprocesos.

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

El siguiente ejemplo es similar al anterior, excepto que usa la clase Task en lugar de un procedimiento de subproceso para generar 50,000 enteros de punto medio aleatorio. En este ejemplo, una expresión lambda reemplaza el procedimiento de hilo GenerateNumbers y la llamada al método Task.WaitAll elimina la necesidad del objeto 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
fuente