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

发布于 2024-11-25 09:13:02 字数 1218 浏览 1 评论 0原文

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

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

考虑以下示例:

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< /a>,因为如果字典已经使用该 id 初始化,则锁会被主动绕过。字典的“Add”方法在锁内调用,因为我们只想调用该方法一次,因为如果尝试添加具有相同键的项目,它会抛出异常。

据我了解,这种锁定模式本质上同步了 Dictionary 的处理方式,这使得它是线程安全的。但是,我收到了一些负面评论,认为这实际上并没有使其线程安全。

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

I got some grief about this in a comment I posted the other day, so I wanted to post the question in an attempt for people to tell me that I'm crazy, which I'll accept, or tell me that I may be right, which I'll also gladly accept. I may also accept anything in between.

Let's say you have a non-thread-safe object type such as Dictionary<int, string>. For the sake of argument, I know you can also use ConcurrentDictionary<int, string> which is thread safe, but I want to talk about the general practice around non-thread-safe objects in a multi-threaded environment.

Consider the following example:

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;
}

The locking pattern is called Double-Checked Locking, since the lock is actively bypassed if the dictionary is already initialized with that id. The "Add" method of the dictionary is called within the lock because we only want to call the method once, because it will throw an exception if you try to add an item with the same key.

It was my understanding that this locking pattern essentially synchronizes the way that Dictionary is handled, which allows it to be thread safe. But, I got some negative comments about how that doesn't actually make it thread safe.

So, my question is, is this locking pattern acceptable for non-thread-safe objects in a multi-threaded environment? If not, what would be a better pattern to use? (assuming there's not an identical C# type that is thread-safe)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

风轻花落早 2024-12-02 09:13:02

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

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

No, this is not safe. The TryGetValue method simply isn't thread-safe, so you shouldn't use it when the object is shared between multiple threads without locking. The double-checked locking pattern involves just testing a reference - which while it isn't guaranteed to give an up to date result, won't cause any other problems. Compare that with TryGetValue which could do anything (e.g. throw an exception, corrupt the internal data structure) if called at the same time as, say, Add.

Personally I'd just use a lock, but you could potentially use ReaderWriterLockSlim. (In most cases a simply lock will be more efficient - but it depends on how long the reading and writing operations take, and what the contentions are like.)

厌味 2024-12-02 09:13:02

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

考虑以下场景:

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

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

This isn't safe, because a second thread can potentially read the value from SomeDictionary while the dictionary is in an inconsistent state.

Consider the following scenario:

  1. Thread A attempts to get id 3. It doesn't exist, so it acquires the lock and calls Add, but is interrupted partway through the method.
  2. Thread B attempts to get id 3. The call to Add has gotten far enough that the method returns (or attempts to return) true.

Now a variety of bad things could happen. It's possible that Thread B sees the first TryGetValue (outside the lock) return true, but the value that's returned is nonsensical because the real value hasn't actually been stored yet. The other possibility is that the Dictionary implementation realizes that it's in an inconsistent state and throws InvalidOperationException. Or it might not throw, it might just continue with a corrupted internal state. Either way, bad mojo.

倾听心声的旋律 2024-12-02 09:13:02

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

Just remove the first TryGetValue and you'll be fine.

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

Do not use ReaderWriterLock or ReaderWriterLockSlim unless you are doing less than 20% writes AND the workload within the lock is significant enough that parallel reads will matter. As an example, the following demonstrates that a simple lock() statement will out-perform the use of either reader/writer locks when the read/write operation is simple.

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();
                }
            }
        }
    }
}

The preceeding program outputs the following results on my PC:

ReaderWriterLock
Completed: 2412
Completed: 2385
Completed: 2422

ReaderWriterLockSlim
Completed: 1374
Completed: 1397
Completed: 1491

Mutex
Completed: 763
Completed: 750
Completed: 758
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文