锁定一个非线程安全的对象,这是可以接受的做法吗?

时间:2011-07-19 19:10:01

标签: c# multithreading locking thread-safety

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

假设您有一个非线程安全的对象类型,例如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初始化字典,则会主动绕过锁定。在锁内调用字典的“添加”方法,因为我们只想调用该方法一次,因为如果您尝试添加具有相同键的项,它将引发异常。

据我所知,这种锁定模式基本上同步了Dictionary的处理方式,这使得它可以是线程安全的。但是,我得到了一些负面评论,说明它实际上是如何使其线程安全的。

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

3 个答案:

答案 0 :(得分:8)

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

就我个人而言,我只是使用锁定,但你可能可能使用ReaderWriterLockSlim。 (在大多数情况下,简单锁定会更有效 - 但这取决于读写操作需要多长时间,以及争用的内容。)

答案 1 :(得分:2)

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

考虑以下情况:

  1. 线程A尝试获取id 3.它不存在,因此它获取锁并调用Add,但在方法的中途被中断。
  2. 线程B尝试获取id 3.对Add的调用已经足够远,该方法返回(或尝试返回)true
  3. 现在可能发生各种各样的坏事。线程B可能会看到第一个TryGetValue(在锁外)返回true,但返回的值是无意义的,因为实际值尚未实际存储。另一种可能性是Dictionary实现意识到它处于不一致状态并抛出InvalidOperationException。或者它可能不会抛出,它可能只是继续损坏的内部状态。无论哪种方式,糟糕的魔力

答案 2 :(得分:1)

只需删除第一个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