【问题标题】:Why lock when reading from a dictionary读字典时为什么要锁
【发布时间】:2011-03-27 06:20:27
【问题描述】:

我对正在阅读的书中的代码清单感到困惑,C# 3 in a Nutshell, on threading。 在应用程序服务器中的线程安全主题中,以下代码作为 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 调用,字典也会更新两次。

通过锁定字典读取可以实现什么?
非常感谢您的所有 cmets 和见解。

【问题讨论】:

    标签: c# multithreading caching dictionary locking


    【解决方案1】:

    Dictionary&lt;TKey, TValue&gt;is not threadsafe

    如果一个线程将一个键写入字典,而另一个线程读取字典,则可能会搞砸。 (比如写操作触发了数组resize,或者两个key发生了hash冲突)

    因此,代码使用锁来防止并发写入。

    【讨论】:

    • 谢谢!这就说得通了。我想真的没有办法确定性地证明它。我尝试删除锁并通过创建新线程在循环中调用它几千次,在更改代码之后,每个线程将始终在读取后写入,但它工作正常。但与大多数线程一样,它对我有用的事实并不意味着它会一直有效。干杯!
    • 单线程设置一次Dictionary,之后只能多线程读取,是否还需要加锁?可能不会,但另一种意见会有用
    【解决方案2】:

    这是访问任何非线程安全结构(如列表、字典、公共共享值等)的常见做法。

    并回答主要问题:锁定读取我们保证字典不会在我们读取它的值时被另一个线程更改。这没有在字典中实现,这就是为什么它被称为非线程安全的:)

    【讨论】:

      【解决方案3】:

      写入字典时存在良性竞争条件;正如您所说,两个线程有​​可能确定缓存中没有匹配的条目。在这种情况下,它们都将从数据库中读取,然后尝试插入。只保留最后一个线程插入的对象;当第一个线程完成后,另一个对象将被垃圾回收。

      对字典的read需要被锁定,因为另一个线程可能同时在写,并且读需要搜索一致的结构。

      请注意,.NET 4.0 中引入的ConcurrentDictionary 几乎取代了这种惯用语。

      【讨论】:

        【解决方案4】:

        如果两个线程同时调用并且id存在,那么它们都会返回正确的用户信息。第一个锁是为了防止像 SLaks 所说的错误 - 如果有人在您尝试阅读字典时正在写入字典,您就会遇到问题。在这种情况下,永远不会到达第二个锁。

        如果两个线程同时调用,并且id不存在,一个线程将锁定并进入TryGetValue,这将返回false并将u设置为默认值。再次锁定第一个锁,以防止 SLaks 描述的错误。此时,第一个线程将释放锁,第二个线程将进入并执行相同操作。然后两者都将 'u' 设置为来自 'RetrieveUser(id)' 的信息;这应该是相同的信息。然后一个线程将锁定字典并将 _users[id] 分配给 u 的值。这第二个锁是为了使两个线程同时尝试将值写入相同的内存位置并破坏该内存。我不知道第二个线程进入分配时会做什么。它要么会提前返回而忽略更新,要么会覆盖第一个线程的现有数据。无论如何,字典将包含相同的信息,因为两个线程都应该从 RetrieveUser 收到相同的数据。

        为了性能,作者比较了两种情况 - 上述情况,当两个线程尝试写入相同的数据时,这种情况非常罕见并阻塞,而第二种情况更可能是两个线程调用请求数据一个需要写入的对象,一个存在的对象。例如,threadA 和 threadB 同时调用,ThreadA 锁定一个不存在的 id。没有理由让 threadB 在 threadA 处理 RetriveUser 时等待查找。这种情况可能比上面描述的重复 id 的可能性要大得多,所以为了性能作者选择不锁定整个块。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2019-06-17
          • 1970-01-01
          • 1970-01-01
          • 2015-03-16
          • 2021-12-23
          • 2018-02-15
          • 2021-10-30
          • 1970-01-01
          相关资源
          最近更新 更多