我对我正在阅读的书中的代码列表,Nutshell中的C#3以及线程感到困惑。 在关于应用程序服务器中的线程安全的主题中,下面的代码是作为UserCache的示例给出的:
static class UserCache
{
static Dictionary< int,User> _users = new Dictionary< int, User>();
internal static User GetUser(int id)
{
User u = null;
lock (_users) // Why lock this???
if (_users.TryGetValue(id, out u))
return u;
u = RetrieveUser(id); //Method to retrieve from databse
lock (_users) _users[id] = u; //Why lock this???
return u;
}
}
作者解释了为什么RetrieveUser方法没有锁定,这是为了避免长时间锁定缓存。
我很困惑为什么要锁定TryGetValue和字典的更新,因为即使上面的字典正在被更新两次,如果2个线程同时使用相同的未检索的id调用。
通过锁定字典读取实现了什么? 非常感谢您提出的所有意见和见解。
答案 0 :(得分:16)
Dictionary<TKey, TValue>
班is not threadsafe。
如果一个线程将一个键写入字典,而另一个线程读取字典,则可能会搞砸。 (例如,如果写操作触发数组调整大小,或者两个键是哈希冲突)
因此,代码使用锁来防止并发写入。
答案 1 :(得分:4)
写字典时有良性竞争条件;正如您所说,有可能两个线程确定缓存中没有匹配的条目。在这种情况下,它们都将从DB读取然后尝试插入。仅保留最后一个线程插入的对象;当第一个线程完成后,另一个对象将被垃圾收集。
读取到字典需要被锁定,因为另一个线程可能同时写入,并且读取需要搜索一致的结构。
请注意,.NET 4.0中引入的ConcurrentDictionary
几乎取代了这种习惯用法。
答案 2 :(得分:1)
这是访问任何非线程安全结构(如列表,字典,常见共享值等)的常见做法。
回答主要问题:锁定读取我们保证在我们读取其值时,另一个线程不会更改字典。这不是在字典中实现的,这就是为什么它被称为非线程安全:)
答案 3 :(得分:0)
如果两个线程同时调用并且id存在,那么它们都将返回正确的用户信息。第一个锁定是为了防止像SLaks所说的错误 - 如果有人在您尝试阅读时写入字典,那么您将遇到问题。在这种情况下,永远不会达到第二个锁。
如果两个线程同时调用并且id不存在,则一个线程将锁定并输入TryGetValue,这将返回false并将u设置为默认值。第一次锁定是为了防止SLaks描述的错误。此时,第一个线程将释放锁定,第二个线程将进入并执行相同操作。然后两者都将'u'设置为'RetrieveUser(id)'中的信息;这应该是相同的信息。然后,一个线程将锁定字典并将_users [id]分配给u的值。第二个锁是这样两个线程试图同时将值写入相同的内存位置并破坏该内存。我不知道第二个线程在进入作业时会做什么。它将提前返回忽略更新,或覆盖第一个线程中的现有数据。无论如何,字典将包含相同的信息,因为两个线程都应该从RetrieveUser中收到“u”中的相同数据。
为了提高性能,auther比较了两种情况 - 上面的场景,这将是非常罕见的,当两个线程尝试写入相同的数据时阻塞,第二个场景,两个线程调用请求数据的可能性更大需要编写的对象,以及存在的对象。例如,threadA和threadB同时调用,ThreadA锁定不存在的id。当threadA正在使用RetriveUser时,没有理由让threadB等待查找。这种情况可能比上面描述的重复ID更有可能,因此对于性能,作者选择不锁定整个块。