【问题标题】:Locking on a non-thread-safe object, is it acceptable practice?锁定非线程安全对象,这是可以接受的做法吗?
【发布时间】:2011-10-08 19:48:25
【问题描述】:

我在前几天发布的评论中对此感到有些悲伤,所以我想发布这个问题,试图让人们告诉我我疯了,我会接受,或者告诉我我可能是对的,我也很乐意接受。我也可以接受介于两者之间的任何东西。

假设您有一个非线程安全的对象类型,例如Dictionary<int, string>。为了争论,我知道你也可以使用线程安全的ConcurrentDictionary<int, string>,但我想谈谈多线程环境中非线程安全对象的一般做法。

考虑以下示例:

private static readonly Dictionary<int, string> SomeDictionary = new Dictionary<int, string>();
private static readonly object LockObj = new object();

public static string GetById(int id)
{
  string result;

  /** Lock Bypass **/
  if (SomeDictionary.TryGetValue(id, out result)
  {
    return result;
  }

  lock (LockObj)
  {
    if (SomeDictionary.TryGetValue(id, out result)
    {
      return result;
    }

    SomeDictionary.Add(id, result = GetSomeString());
  }

  return result;
}

锁定模式称为Double-Checked Locking,因为如果字典已使用该ID 初始化,则会主动绕过锁定。字典的“Add”方法是在锁内调用的,因为我们只想调用该方法一次,因为如果你尝试用相同的键添加一个项目,它会抛出异常。

据我了解,这种锁定模式本质上同步了 Dictionary 的处理方式,这使其成为线程安全的。但是,我得到了一些负面评价,认为这实际上并不能保证线程安全。

那么,我的问题是,这种锁定模式对于多线程环境中的非线程安全对象是否可以接受?如果不是,那么使用什么模式会更好? (假设没有相同的 C# 类型是线程安全的)

【问题讨论】:

标签: c# multithreading locking thread-safety


【解决方案1】:

不,这是安全的。 TryGetValue 方法根本不是线程安全的,因此当对象在多个线程之间共享而没有锁定时,您不应该使用它。双重检查锁定模式仅涉及测试引用 - 虽然不能保证提供最新结果,但 不会 导致任何其他问题。与 TryGetValue 相比,如果与 Add 同时调用,它可以做任何事情(例如抛出异常、破坏内部数据结构)。

我个人只会使用锁,但您可以可能使用ReaderWriterLockSlim。 (在大多数情况下,简单的锁会更有效——但这取决于读写操作需要多长时间,以及争用情况如何。)

【讨论】:

  • 我想我明白你在说什么。忽略 Dictionary 一秒钟,如果只有一个名为“IsInitialized”的布尔值,那么这将是执行双重检查锁定模式的正确方法吗?此外,关于 Dictionary,如果您使用“ContainsKey”而不是“TryGetValue”,这会是进行第一次检查的更好方法吗?或者你可以不相信任何字典中的线程安全方法吗?
  • @Mike:是的,检查一个布尔变量就可以了——尽管我个人一开始并不喜欢 DCL 模式。不,使用 Dictionary.ContainsKey 不会更好。该类被简单记录为不是线程安全的。
  • @Jon Skeet,“……虽然我个人一开始并不喜欢 DCL 模式。”仅此一项就 +1。
【解决方案2】:

这是不安全的,因为当字典处于不一致状态时,第二个线程可能会从 SomeDictionary 读取值。

考虑以下场景:

  1. 线程 A 尝试获取 id 3。它不存在,因此它获取锁并调用 Add,但在该方法的中途被中断。
  2. 线程 B 尝试获取 id 3。对 Add 的调用已经足够远,以至于方法返回(或尝试返回)true

现在可能会发生各种不好的事情。线程 B 可能看到第一个 TryGetValue(在锁之外)返回 true,但返回的值是无意义的,因为实际值尚未实际存储。另一种可能性是 Dictionary 实现意识到它处于不一致状态并抛出InvalidOperationException。或者它可能不会抛出,它可能只是以损坏的内部状态继续。不管怎样,坏魔力

【讨论】:

    【解决方案3】:

    只需删除第一个 TryGetValue 即可。

    /** Lock Bypass **/
    if (SomeDictionary.TryGetValue(id, out result)
    {
        return result;
    }
    

    请勿使用 ReaderWriterLock 或 ReaderWriterLockSlim,除非您执行的写入少于 20% 并且锁内的工作负载足够大以至于并行读取很重要。作为一个例子,下面展示了一个简单的 lock() 语句在读/写操作很简单的情况下将胜过使用读/写锁。

    internal class MutexOrRWLock
    {
        private const int LIMIT = 1000000;
        private const int WRITE = 100;//write once every n reads
    
        private static void Main()
        {
            if (Environment.ProcessorCount < 8)
                throw new ApplicationException("You must have at least 8 cores.");
            Process.GetCurrentProcess().ProcessorAffinity = new IntPtr(255); // pin the process to first 8 CPUs
    
            Console.WriteLine("ReaderWriterLock");
            new RWLockTest().Test(3);
            Console.WriteLine("ReaderWriterLockSlim");
            new RWSlimTest().Test(3);
            Console.WriteLine("Mutex");
            new MutexTest().Test(3);
        }
    
        private class RWLockTest : MutexTest
        {
            private readonly ReaderWriterLock _lock1 = new ReaderWriterLock();
    
            protected override void BeginRead() { _lock1.AcquireReaderLock(-1); }
            protected override void EndRead() { _lock1.ReleaseReaderLock(); }
    
            protected override void BeginWrite() { _lock1.AcquireWriterLock(-1); }
            protected override void EndWrite() { _lock1.ReleaseWriterLock(); }
        }
    
        private class RWSlimTest : MutexTest
        {
            private readonly ReaderWriterLockSlim _lock1 = new ReaderWriterLockSlim();
    
            protected override void BeginRead() { _lock1.EnterReadLock(); }
            protected override void EndRead() { _lock1.ExitReadLock(); }
    
            protected override void BeginWrite() { _lock1.EnterWriteLock(); }
            protected override void EndWrite() { _lock1.ExitWriteLock(); }
        }
    
        private class MutexTest
        {
            private readonly ManualResetEvent start = new ManualResetEvent(false);
            private readonly Dictionary<int, int> _data = new Dictionary<int, int>();
    
            public void Test(int count)
            {
                for (int i = 0; i < count; i++)
                {
                    _data.Clear();
                    for (int val = 0; val < LIMIT; val += 3)
                        _data[val] = val;
    
                        start.Reset();
                    Thread[] threads = new Thread[8];
                    for (int ti = 0; ti < 8; ti++)
                        (threads[ti] = new Thread(Work)).Start();
    
                    Thread.Sleep(1000);
                    Stopwatch sw = new Stopwatch();
                    sw.Start();
                    start.Set();
                    foreach (Thread t in threads)
                        t.Join();
                    sw.Stop();
                    Console.WriteLine("Completed: {0}", sw.ElapsedMilliseconds);
                }
            }
    
            protected virtual void BeginRead() { Monitor.Enter(this); }
            protected virtual void EndRead() { Monitor.Exit(this); }
    
            protected virtual void BeginWrite() { Monitor.Enter(this); }
            protected virtual void EndWrite() { Monitor.Exit(this); }
    
            private void Work()
            {
                int val;
                Random r = new Random();
                start.WaitOne();
                for (int i = 0; i < LIMIT; i++)
                {
                    if (i % WRITE == 0)
                    {
                        BeginWrite();
                        _data[r.Next(LIMIT)] = i;
                        EndWrite();
                    }
                    else
                    {
                        BeginRead();
                        _data.TryGetValue(i, out val);
                        EndRead();
                    }
                }
            }
        }
    }
    

    前面的程序在我的电脑上输出以下结果:

    ReaderWriterLock
    Completed: 2412
    Completed: 2385
    Completed: 2422
    
    ReaderWriterLockSlim
    Completed: 1374
    Completed: 1397
    Completed: 1491
    
    Mutex
    Completed: 763
    Completed: 750
    Completed: 758
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-11-19
      • 1970-01-01
      • 2011-10-07
      • 2011-03-20
      • 2021-02-08
      • 1970-01-01
      • 2012-02-14
      • 1970-01-01
      相关资源
      最近更新 更多