我在前几天发表的评论中对此感到悲伤,所以我想发布这个问题,试图让人们告诉我,我疯了,我会接受,或者告诉我,我可能是对的,我也很乐意接受。我也可以接受介于两者之间的任何事情。
假设您有一个非线程安全的对象类型,例如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#类型是线程安全的)
答案 0 :(得分:8)
不,这是不安全。 TryGetValue
方法根本不是线程安全的,因此当多个线程之间共享对象而不进行锁定时,不应使用它。双重检查锁定模式仅涉及测试引用 - 虽然不能保证提供最新结果,但不会导致任何其他问题。将其与TryGetValue
进行比较可以做任何事情(例如抛出异常,破坏内部数据结构),如果与Add
同时调用。
就我个人而言,我只是使用锁定,但你可能可能使用ReaderWriterLockSlim
。 (在大多数情况下,简单锁定会更有效 - 但这取决于读写操作需要多长时间,以及争用的内容。)
答案 1 :(得分:2)
这不安全,因为当字典处于不一致状态时,第二个线程可能会从SomeDictionary
读取值。
考虑以下情况:
Add
,但在方法的中途被中断。Add
的调用已经足够远,该方法返回(或尝试返回)true
。现在可能发生各种各样的坏事。线程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