7

如果我有一个 ConcurrentDictionary 并在 if 语句中使用 TryGetValue,这是否会使 if 语句的内容线程安全?还是必须在 if 语句中锁定?

例子:

        ConcurrentDictionary<Guid, Client> m_Clients;

        Client client;
        //Does this if make the contents within it thread-safe? 
        if (m_Clients.TryGetValue(clientGUID, out client))
        {
            //Users is a list.
            client.Users.Add(item);
        }

还是我必须这样做:

        ConcurrentDictionary<Guid, Client> m_Clients;

        Client client;
        //Does this if make the contents within it thread-safe? 
        if (m_Clients.TryGetValue(clientGUID, out client))
        {
            lock (client)
            {
                //Users is a list.
                client.Users.Add(item);
            }
        }
4

3 回答 3

7

是的,您必须锁定在 if 语句中,从并发字典中获得的唯一保证是它的方法是线程保存的。

于 2012-11-07T20:49:31.763 回答
2

接受的答案可能会产生误导,具体取决于您的观点和您试图实现的线程安全范围。这个答案是针对那些在学习线程和并发时偶然发现这个问题的人:

确实,锁定字典检索的输出(客户端对象)使某些代码线程安全,但只有在锁定中访问检索对象的代码。在该示例中,另一个线程可能会在当前线程检索到该对象从字典中删除该对象。(即使在检索和锁定之间没有语句,其他线程仍然可以在两者之间执行。)然后,此代码会将 Client 对象添加到 Users 列表中,即使它不再在并发字典中。这可能会导致异常、同步或竞争条件。

这取决于程序的其余部分在做什么。但是在我描述的场景中,将锁放在整个字典检索周围会更安全。然后一个普通的字典可能比并发字典更快更简单,只要你在使用它时总是锁定它!

于 2017-08-22T23:01:50.217 回答
2

虽然目前的两个答案在技术上都是正确的,但我认为它们可能会产生一些误导,并且它们并没有表达 ConcurrentDictionary 的强大优势。也许 OP 解决锁问题的原始方法在特定情况下有效,但这个答案更普遍地针对第一次学习 ConcurrentDictionary 的人。

并发字典的设计使您不必使用锁。它有几种特殊的方法,围绕着这样的想法设计的,即当您当前正在处理字典中的对象时,一些其他线程可以修改字典中的对象。举个简单的例子,TryUpdate 方法可以让您检查一个键的值是否在您获得它和您尝试更新它的那一刻之间发生了变化。如果您获得的值与当前在 ConcurrentDictionary 中的值匹配,您可以更新它并且 TryUpdate 返回 true。如果不是,TryUpdate 将返回 false。TryUpdate 方法的文档可能会使这有点混乱,因为它没有明确说明为什么存在比较值,但这就是比较值背后的想法。如果您想对添加或更新进行更多控制,则可以使用AddOrUpdate 方法的重载之一来为键添加值(如果在您尝试添加它时它不存在)如果其他线程已经为指定的键添加了值,则更新该值。您尝试做的任何事情的上下文将决定使用适当的方法。关键是,与其锁定,不如尝试查看 ConcurrentDictionary 提供的特殊方法,并更喜欢那些尝试提出自己的锁定解决方案的方法。

对于 OP 的原始问题,我建议不要这样:

    ConcurrentDictionary<Guid, Client> m_Clients;

    Client client;
    //Does this if make the contents within it thread-safe? 
    if (m_Clients.TryGetValue(clientGUID, out client))
    {
        //Users is a list.
        client.Users.Add(item);
    }

可以尝试以下方法*:

    ConcurrentDictionary<Guid, Client> m_Clients;

    Client originalClient;
    if(m_Clients.TryGetValue(clientGUID, out originalClient)
    {
        //The Client object will need to implement IEquatable if more
        //than an object instance comparison needs to be done.  This
        //sample code assumes that Client implements IEquatable.

        //If copying a Client is not trivial, you'll probably want to
        //also implement a simple type of copy in a method of the Client
        //object.  This sample code assumes that the Client object has
        //a ShallowCopy method to do this copy for simplicity's sake.
        Client modifiedClient = originalClient.ShallowCopy();

        //Make whatever modifications to modifiedClient that need to get
        //made...
        modifiedClient.Users.Add(item);
        
        //Now update the value in the ConcurrentDictionary
        if(!m_Clients.TryUpdate(clientGuid, modifiedClient, originalClient))
        {
            //Do something if the Client object was updated in between
            //when it was retrieved and when the code here tries to
            //modify it.
        }
    }

*请注意,在上面的示例中,我使用 TryUpate 是为了便于演示该概念。在实践中,如果您需要确保在对象不存在时添加对象或在对象存在时更新对象,则 AddOrUpdate 方法将是理想的选择,因为该方法处理检查添加与更新所需的所有循环和采取适当的行动。

一开始可能看起来有点困难,因为可能需要实现 IEquatable ,并且根据需要复制 Client 实例的方式,某种复制功能,但从长远来看,如果你正在使用它,它会得到回报ConcurrentDictionary 和其中的对象以任何严肃的方式。

于 2021-08-13T20:17:31.943 回答