C # Thread Safe быстрый (EST) счетчик

147

Как получить потокобезопасный счетчик в C # с наилучшей производительностью?

Это так просто, как получается:

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

Но есть ли более быстрые альтернативы?

JohnDoDo
источник

Ответы:

108

В соответствии с рекомендациями других, производительность Interlocked.Incrementбудет выше, чем у lock(). Достаточно взглянуть на IL и Assembly, где вы увидите, что это Incrementпревращается в оператор «блокировки шины», а его переменная напрямую увеличивается (x86) или «добавляется» к (x64).

Этот оператор "bus lock" блокирует шину, чтобы предотвратить доступ к ней другого ЦП, когда вызывающий ЦП выполняет свою работу. Теперь взгляните на lock()IL в C # оператора. Здесь вы увидите звонки Monitor, чтобы начать или закончить раздел.

Другими словами, .Net lock()заявление делает гораздо больше, чем .Net Interlocked.Increment.

Так что, если все, что вы хотите сделать, это Interlock.Incrementувеличить переменную, будет быстрее. Просмотрите все методы блокировки, чтобы увидеть различные доступные атомарные операции и найти те, которые соответствуют вашим потребностям. Используйте, lock()если вы хотите сделать более сложные вещи, такие как множественные взаимосвязанные приращения / уменьшения или сериализовать доступ к ресурсам, которые являются более сложными, чем целые числа.

Les
источник
3
-1 для деталей реализации. Это правда, что блокировка намного медленнее, чем атомная операция, но это не имеет никакого отношения к IL. Эти вызовы функций были бы намного быстрее, чем атомарные операции, если бы не их семантика, которая по сути не требуется для IL.
щенок
33

Я предлагаю вам использовать встроенный инкремент блокировки .NET в библиотеке System.Threading.

Следующий код будет увеличивать длинную переменную по ссылке и полностью безопасен для потоков:

Interlocked.Increment(ref myNum);

Источник: http://msdn.microsoft.com/en-us/library/dd78zt0c.aspx

Андрей Белый
источник
1

Как уже упоминалось, использовать Interlocked.Increment

Пример кода от MS:

В следующем примере определяется, сколько случайных чисел в диапазоне от 0 до 1000 требуется для генерации 1000 случайных чисел со значением средней точки. Чтобы отслеживать количество значений средней точки, переменная midpointCount устанавливается равной 0 и увеличивается каждый раз, когда генератор случайных чисел возвращает значение средней точки, пока оно не достигнет 10000. Поскольку три потока генерируют случайные числа, вызывается метод Increment (Int32), чтобы несколько потоков не обновляли midpointCount одновременно. Обратите внимание, что блокировка также используется для защиты генератора случайных чисел, и что объект CountdownEvent используется для гарантии того, что метод Main не завершит выполнение до трех потоков.

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

Следующий пример аналогичен предыдущему, за исключением того, что он использует класс Task вместо процедуры потока для генерации 50 000 случайных целых чисел средней точки. В этом примере лямбда-выражение заменяет процедуру потока GenerateNumbers, а вызов метода Task.WaitAll устраняет необходимость в объекте 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
источник