C # Thread safe fast (est) 카운터


147

최상의 성능으로 C #에서 스레드 안전 카운터를 얻는 방법은 무엇입니까?

이것은 얻는 것처럼 간단합니다.

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

그러나 더 빠른 대안이 있습니까?

답변:



108

다른 사람들이 권장하는 Interlocked.Increment것보다 성능이 더 좋습니다 lock(). IL 및 어셈블리를 살펴보면 Increment"bus lock"문으로 바뀌고 변수가 직접 증가 (x86)되거나 "x64"에 추가되는 것을 볼 수 있습니다.

이 "bus lock"문은 호출 CPU가 작동하는 동안 다른 CPU가 버스에 액세스하지 못하도록 버스를 잠급니다. 이제 C # lock()문의 IL을 살펴보십시오 . 여기 Monitor에서 섹션을 시작하거나 종료하기위한 호출이 표시됩니다 .

즉, .Net lock()문은 .Net보다 많은 일을하고 있습니다 Interlocked.Increment.

따라서 변수를 늘리는 것만 큼 Interlock.Increment빨라집니다. 사용 가능한 다양한 원자 연산을 확인하고 필요에 맞는 연산을 찾으려면 모든 연동 방법을 검토하십시오. lock()여러 관련 증가 / 감소와 같은보다 복잡한 작업을 수행하거나 정수보다 복잡한 리소스에 대한 액세스를 직렬화하려는 경우에 사용하십시오 .


3
구현 세부 사항은 -1입니다. 잠금이 원자 연산보다 느리다는 것은 사실이지만 IL과는 아무런 관련이 없습니다. 이러한 기능 호출은 의미론이 아니라면 원자 연산보다 훨씬 빠르며 이는 본질적으로 IL에 필요하지 않습니다.
강아지



1

이미 언급했듯이 Interlocked.Increment

MS의 코드 예제 :

다음 예제는 중간 점 값으로 1,000 개의 난수를 생성하는 데 필요한 0에서 1,000 사이의 난수를 결정합니다. 중간 점 값의 수를 추적하기 위해 변수 midpointCount가 0으로 설정되고 난수 생성기가 10,000에 도달 할 때까지 난수 생성기가 중간 점 값을 리턴 할 때마다 증가합니다. 세 개의 스레드가 난수를 생성하므로 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

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.