C # penghitung thread safe fast (est)

147

Apa cara untuk mendapatkan penghitung aman thread di C # dengan kinerja terbaik?

Ini sesederhana yang didapat:

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

Tetapi apakah ada alternatif yang lebih cepat?

JohnDoDo
sumber

Jawaban:

108

Seperti yang direkomendasikan oleh orang lain, kinerja Interlocked.Incrementakan lebih baik daripada lock(). Lihat saja IL dan Assembly di mana Anda akan melihat bahwa itu Incrementberubah menjadi pernyataan "kunci bus" dan variabelnya langsung bertambah (x86) atau "ditambahkan" ke (x64).

Pernyataan "kunci bus" ini mengunci bus untuk mencegah CPU lain mengakses bus saat CPU pemanggil melakukan operasinya. Sekarang, lihat lock()IL pernyataan C # . Di sini Anda akan melihat panggilan Monitoruntuk memulai atau mengakhiri bagian.

Dengan kata lain, lock()pernyataan .Net melakukan lebih banyak daripada .Net Interlocked.Increment.

SO, jika semua yang Anda ingin lakukan adalah peningkatan variabel, Interlock.Incrementakan lebih cepat. Tinjau semua metode yang saling terkait untuk melihat berbagai operasi atom yang tersedia dan untuk menemukan yang sesuai dengan kebutuhan Anda. Gunakan lock()ketika Anda ingin melakukan hal-hal yang lebih kompleks seperti beberapa kenaikan / penurunan yang saling terkait, atau untuk membuat serial akses ke sumber daya yang lebih kompleks daripada bilangan bulat.

Les
sumber
3
-1 untuk detail implementasi. Memang benar bahwa penguncian jauh lebih lambat daripada operasi atom, tetapi ini tidak ada hubungannya dengan IL. Pemanggilan fungsi itu akan jauh lebih cepat daripada operasi atom jika bukan karena semantiknya, yang pada dasarnya tidak diperlukan dari IL.
Puppy
33

Saya sarankan Anda menggunakan kenaikan interlock built in NET di perpustakaan System.Threading.

Kode berikut akan menambah variabel panjang dengan referensi dan sepenuhnya aman untuk thread:

Interlocked.Increment(ref myNum);

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

Andrew White
sumber
1

Seperti yang sudah disebutkan digunakan Interlocked.Increment

Contoh kode dari MS:

Contoh berikut menentukan berapa banyak angka acak yang berkisar dari 0 hingga 1.000 diperlukan untuk menghasilkan 1.000 angka acak dengan nilai titik tengah. Untuk melacak jumlah nilai titik tengah, variabel, midpointCount, disetel sama dengan 0 dan bertambah setiap kali generator angka acak mengembalikan nilai titik tengah hingga mencapai 10.000. Karena tiga utas menghasilkan angka acak, metode Penambahan (Int32) dipanggil untuk memastikan bahwa beberapa utas tidak memperbarui midpointCount secara bersamaan. Perhatikan bahwa kunci juga digunakan untuk melindungi generator angka acak, dan bahwa objek CountdownEvent digunakan untuk memastikan bahwa metode Utama tidak menyelesaikan eksekusi sebelum tiga utas.

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

Contoh berikut ini mirip dengan yang sebelumnya, kecuali bahwa ia menggunakan kelas Tugas alih-alih prosedur utas untuk menghasilkan 50.000 bilangan bulat titik tengah acak. Dalam contoh ini, ekspresi lambda menggantikan prosedur thread GenerateNumbers, dan panggilan ke metode Task.WaitAll menghilangkan kebutuhan untuk objek 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
sumber