【问题标题】:Best C# solution for multithreaded threadsafe read/write locking?多线程线程安全读/写锁定的最佳 C# 解决方案?
【发布时间】:2009-08-25 19:42:48
【问题描述】:

在 C# 的多线程环境中锁定对static 成员的读/写访问的最安全(也是最短)的方法是什么?

是否可以在类级别进行线程安全锁定和解锁(这样我就不会在每次需要静态成员访问时都重复锁定/解锁代码)?

编辑:示例代码会很棒:)

编辑:我应该使用 volatile 关键字还是 Thread.MemoryBarrier() 来避免多处理器缓存,还是没有必要?根据 Jon Skeet 的说法,只有那些会使更改对其他处理器可见? (单独问这个here)。

【问题讨论】:

    标签: c# .net multithreading


    【解决方案1】:

    小值

    对于小值(基本上可以声明为 volatile 的任何字段),您可以执行以下操作:

    private static volatile int backingField;
    
    public static int Field
    {
        get { return backingField; }
        set { backingField = value; }
    } 
    

    大值

    对于较大的值,如果值大于 32 位计算机上的 32 位或 64 位计算机上的 64 位,则分配将不是原子的。请参阅ECMA 335 12.6.6 规范。所以对于引用类型和大多数内置值类型,赋值是原子的,但是如果你有一些大的结构,比如:

    struct BigStruct 
    {
        public long value1, valuea0a, valuea0b, valuea0c, valuea0d, valuea0e;
        public long value2, valuea0f, valuea0g, valuea0h, valuea0i, valuea0j;
        public long value3;
    }
    

    在这种情况下,您需要对 get 访问器进行某种锁定。您可以为此使用ReaderWriterLockSlim,我在下面演示了这一点。 Joe Duffy 有 advice 使用 ReaderWriterLockSlimReaderWriterLock

        private static BigStruct notSafeField;
        private static readonly ReaderWriterLockSlim slimLock = 
            new ReaderWriterLockSlim();
    
        public static BigStruct Safe
        {
            get
            {
                slimLock.EnterReadLock();
                var returnValue = notSafeField;
                slimLock.ExitReadLock();
    
                return returnValue;
            }
            set
            {
                slimLock.EnterWriteLock();
                notSafeField = value;
                slimLock.ExitWriteLock();
            }
        }
    

    不安全的 Get-Accessor 演示

    这是我用来显示在 get-accessor 中不使用锁时缺乏原子性的代码:

        private static readonly object mutexLock = new object();
        private static BigStruct notSafeField;
    
        public static BigStruct NotSafe
        {
            get
            {
                // this operation is not atomic and not safe
                return notSafeField;
            }
            set
            {
                lock (mutexLock)
                {
                    notSafeField = value;
                }
            }
        }
    
        public static void Main(string[] args)
        {
            var t = new Thread(() =>
                {
                    while (true)
                    {
                        var current = NotSafe;
                        if (current.value2 != (current.value1 * 2)
                            || current.value3 != (current.value1 * 5))
                        {
                            throw new Exception(String.Format("{0},{1},{2}", current.value1, current.value2, current.value3));
                        }
                    }
                });
            t.Start();
            for(int i=0; i<50; ++i)
            {
                var w = new Thread((state) =>
                    {
                        while(true)
                        {
                            var index = (int) state;
                            var newvalue = new BigStruct();
                            newvalue.value1 = index;
                            newvalue.value2 = index * 2;
                            newvalue.value3 = index * 5;
                            NotSafe = newvalue;
                        }
                    });
                w.Start(i);
            }
            Console.ReadLine();
        }
    

    【讨论】:

    • 注意volatile 关键字:正如this detailed article on threading 中提到的那样,在易失性字段上的写入和读取操作仍然可以重新排序,从而导致非常微妙、难以发现的错误.
    • P.S.:对这类锁使用 try/finally 是个好主意。您不希望以引发的异常结束,从而使事物保持锁定状态。
    【解决方案2】:

    最安全和最短的方法是创建一个类型为Object 的私有静态字段,该字段仅用于锁定(将其视为“挂锁”对象)。使用此字段并仅使用此字段进行锁定,因为这可以防止其他类型在锁定您的代码时锁定您的代码。

    如果您锁定类型本身,则存在另一种类型也决定锁定您的类型的风险,这可能会造成死锁。

    这是一个例子:

    class Test
    {
        static readonly Object fooLock = new Object();
        static String foo;
    
        public static String Foo
        {
            get { return foo; }
            set
            {
                lock (fooLock)
                {
                    foo = value;
                }
            }
        }
    }
    

    请注意,我创建了一个用于锁定 foo 的私​​有静态字段 - 我使用该字段来锁定对该字段的写入操作。

    【讨论】:

    • 我需要使用 volatile 关键字来避免变量的多处理器缓存还是可以忽略这一点?
    • 如果您锁定,则需要 volatile(并且需要非常小心)。
    • “get”也应该是lock了吧?
    • 这个例子中的锁是没用的,不会阻止任何竞争条件......正确的方法是在类级别根本不同步,而是提供一个所有线程都应该锁定的 SyncRoot 对象在对该属性进行读写操作之前。
    • 同意。我不会在任何需要读/写锁定的代码中使用这个答案。 @JosephKingry 有更好的答案。
    【解决方案3】:

    虽然你可以只使用一个互斥锁来控制对类的所有访问(有效地序列化对类的访问)我建议你研究静态类,确定哪些成员在哪里以及如何使用以及使用一个或多个ReaderWriterLock(MSDN 文档中的代码示例),提供对多个阅读器的访问,但同时只有一个作者。

    这样,您将拥有一个细粒度的多线程类,它只会阻止写入,但同时允许多个读取器,并且允许在读取另一个不相关成员的同时写入一个成员。

    【讨论】:

      【解决方案4】:
      class LockExample {
          static object lockObject = new object();
          static int _backingField = 17;
      
          public static void NeedsLocking() {
              lock(lockObject) {
                  // threadsafe now
              }
          }
      
          public static int ReadWritePropertyThatNeedsLocking {
              get {
                  lock(lockObject) {
                      // threadsafe now
                      return _backingField;
                  }
              }
              set {
                  lock(lockObject) {
                      // threadsafe now
                      _backingField = value;
                  }
              }
          }
      }
      

      lock 用于专门为此目的创建的对象,而不是typeof(LockExample),以防止其他人锁定LockExample 的类型对象的死锁情况。

      是否可以在类级别进行线程安全锁定和解锁(因此我不会在每次需要静态成员访问时都重复锁定/解锁代码)?

      仅在您需要的地方使用lock,并在被调用者内部执行,而不是要求调用者执行locking。

      【讨论】:

      • 正如 cmets 对 Andrew Hare 的回答所说,这里的锁在给定一个 int 值字段的情况下没有做任何事情。
      • @Jason,您的代码不允许超过 1 个线程读取该值。多线程读取值有什么问题?
      【解决方案5】:

      其他几个人已经解释了如何将 lock 关键字与私有锁对象一起使用,所以我将添加以下内容:

      请注意,即使您锁定类型中的每个方法,在一个序列中调用多个方法也不能被认为是原子的。例如,如果你正在实现一个字典并且你的接口有一个 Contains 方法和一个 Add 方法,那么在 Add 之后调用 Contains 将 不是 是原子的。有人可以修改对 Contains 和 Add 的调用之间的字典 - 即存在竞争条件。要解决此问题,您必须更改接口并提供像 AddIfNotPresent(或类似)这样的方法,它将检查和修改封装为单个操作。

      Jared Par 有一个出色的 blog post on the topic(请务必阅读 cmets)。

      【讨论】:

        【解决方案6】:

        应该根据需要在静态访问器中锁定/解锁每个静态成员访问。

        保留一个私有对象用于锁定,并根据需要进行锁定。这使锁定尽可能细粒度,这非常重要。它还将锁定保持在静态类成员的内部。如果您在类级别锁定,您的调用者将负责锁定,这会损害可用性。

        【讨论】:

          【解决方案7】:

          感谢大家,我很高兴分享这个演示程序,受上述贡献的启发,它运行 3 种模式(不安全、互斥、苗条)。

          请注意,设置“Silent = false”将导致线程之间完全没有冲突。使用此“Silent = false”选项使所有线程都写入控制台。

          using System;
          using System.Collections.Generic;
          using System.Linq;
          using System.Text;
          using System.Threading;
          
          namespace Test
          {
              class Program
              {
                  //------------------------------------------------------------------------------  
                  // Configuration.
          
                  const bool Silent = true;
          
                  const int Nb_Reading_Threads = 8;
                  const int Nb_Writing_Threads = 8;
          
                  //------------------------------------------------------------------------------  
                  // Structured data.
          
                  public class Data_Set
                  {
                      public const int Size = 20;
                      public long[] T;
          
                      public Data_Set(long t)
                      {
                          T = new long[Size];
          
                          for (int i = 0; i < Size; i++)
                              T[i] = t;
                      }
          
                      public Data_Set(Data_Set DS)
                      {
                          Set(DS);
                      }
          
                      public void Set(Data_Set DS)
                      {
                          T = new long[Size];
          
                          for (int i = 0; i < Size; i++)
                              T[i] = DS.T[i];
                      }
                  }
          
                  private static Data_Set Data_Sample = new Data_Set(9999);
          
                  //------------------------------------------------------------------------------  
                  // SAFE process.
          
                  public enum Mode { Unsafe, Mutex, Slim };
                  public static Mode Lock_Mode = Mode.Unsafe;
          
                  private static readonly object Mutex_Lock = new object();
                  private static readonly ReaderWriterLockSlim Slim_Lock = new ReaderWriterLockSlim();
          
                  public static Data_Set Safe_Data
                  {
                      get
                      {
                          switch (Lock_Mode)
                          {
                              case Mode.Mutex:
          
                                  lock (Mutex_Lock)
                                  {
                                      return new Data_Set(Data_Sample);
                                  }
          
                              case Mode.Slim:
          
                                  Slim_Lock.EnterReadLock();
                                  Data_Set DS = new Data_Set(Data_Sample);
                                  Slim_Lock.ExitReadLock();
          
                                  return DS;
          
                              default:
          
                                  return new Data_Set(Data_Sample);
                          }
                      }
                      set
                      {
                          switch (Lock_Mode)
                          {
                              case Mode.Mutex:
          
                                  lock (Mutex_Lock)
                                  {
                                      Data_Sample.Set(value);
                                  }
                                  break;
          
                              case Mode.Slim:
          
                                  Slim_Lock.EnterWriteLock();
                                  Data_Sample.Set(value);
                                  Slim_Lock.ExitWriteLock();
                                  break;
          
                              default:
          
                                  Data_Sample.Set(value);
                                  break;
                          }
                      }
                  }
          
                  //------------------------------------------------------------------------------  
                  // Main function.
          
                  static void Main(string[] args)
                  {
                      // Console.
                      const int Columns = 120;
                      const int Lines = (Silent ? 50 : 500);
          
                      Console.SetBufferSize(Columns, Lines);
                      Console.SetWindowSize(Columns, 40);
          
                      // Threads.
                      const int Nb_Threads = Nb_Reading_Threads + Nb_Writing_Threads;
                      const int Max = (Silent ? 50000 : (Columns * (Lines - 5 - (3 * Nb_Threads))) / Nb_Threads);
          
                      while (true)
                      {
                          // Console.
                          Console.Clear();
                          Console.WriteLine("");
          
                          switch (Lock_Mode)
                          {
                              case Mode.Mutex:
          
                                  Console.WriteLine("---------- Mutex ----------");
                                  break;
          
                              case Mode.Slim:
          
                                  Console.WriteLine("---------- Slim ----------");
                                  break;
          
                              default:
          
                                  Console.WriteLine("---------- Unsafe ----------");
                                  break;
                          }
          
                          Console.WriteLine("");
                          Console.WriteLine(Nb_Reading_Threads + " reading threads + " + Nb_Writing_Threads + " writing threads");
                          Console.WriteLine("");
          
                          // Flags to monitor all threads.
                          bool[] Completed = new bool[Nb_Threads];
          
                          for (int i = 0; i < Nb_Threads; i++)
                              Completed[i] = false;
          
                          // Threads that change the values.
                          for (int W = 0; W < Nb_Writing_Threads; W++)
                          {
                              var Writing_Thread = new Thread((state) =>
                              {
                                  int t = (int)state;
                                  int u = t % 10;
          
                                  Data_Set DS = new Data_Set(t + 1);
          
                                  try
                                  {
                                      for (int k = 0; k < Max; k++)
                                      {
                                          Safe_Data = DS;
          
                                          if (!Silent) Console.Write(u);
                                      }
                                  }
                                  catch (Exception ex)
                                  {
                                      Console.WriteLine("\r\n" + "Writing thread " + (t + 1) + " / " + ex.Message + "\r\n");
                                  }
          
                                  Completed[Nb_Reading_Threads + t] = true;
                              });
          
                              Writing_Thread.Start(W);
                          }
          
                          // Threads that read the values.
                          for (int R = 0; R < Nb_Reading_Threads; R++)
                          {
                              var Reading_Thread = new Thread((state) =>
                              {
                                  int t = (int)state;
                                  char u = (char)((int)('A') + (t % 10));
          
                                  try
                                  {
                                      for (int j = 0; j < Max; j++)
                                      {
                                          Data_Set DS = Safe_Data;
          
                                          for (int i = 0; i < Data_Set.Size; i++)
                                          {
                                              if (DS.T[i] != DS.T[0])
                                              {
                                                  string Log = "";
          
                                                  for (int k = 0; k < Data_Set.Size; k++)
                                                      Log += DS.T[k] + " ";
          
                                                  throw new Exception("Iteration " + (i + 1) + "\r\n" + Log);
                                              }
                                          }
          
                                          if (!Silent) Console.Write(u);
                                      }
                                  }
                                  catch (Exception ex)
                                  {
                                      Console.WriteLine("\r\n" + "Reading thread " + (t + 1) + " / " + ex.Message + "\r\n");
                                  }
          
                                  Completed[t] = true;
                              });
          
                              Reading_Thread.Start(R);
                          }
          
                          // Wait for all threads to complete.
                          bool All_Completed = false;
          
                          while (!All_Completed)
                          {
                              All_Completed = true;
          
                              for (int i = 0; i < Nb_Threads; i++)
                                  All_Completed &= Completed[i];
                          }
          
                          // END.
                          Console.WriteLine("");
                          Console.WriteLine("Done!");
                          Console.ReadLine();
          
                          // Toogle mode.
                          switch (Lock_Mode)
                          {
                              case Mode.Unsafe:
          
                                  Lock_Mode = Mode.Mutex;
                                  break;
          
                              case Mode.Mutex:
          
                                  Lock_Mode = Mode.Slim;
                                  break;
          
                              case Mode.Slim:
          
                                  Lock_Mode = Mode.Unsafe;
                                  break;
                          }
                      }
                  }
              }
          }
          

          【讨论】:

            【解决方案8】:

            锁定静态方法听起来是个坏主意,一方面,如果您从类构造函数中使用这些静态方法,您可能会因为加载器锁而遇到一些有趣的副作用(事实上,类加载器可以忽略其他锁) .

            【讨论】:

              猜你喜欢
              • 2017-07-24
              • 1970-01-01
              • 2019-05-17
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2011-06-09
              • 2017-06-19
              相关资源
              最近更新 更多