【问题标题】:Using string as a lock to do thread synchronization使用字符串作为锁做线程同步
【发布时间】:2011-05-10 17:29:10
【问题描述】:

在查看一些遗留应用程序代码时,我注意到它使用字符串对象进行线程同步。我正在尝试解决该程序中的一些线程争用问题,并且想知道这是否会导致一些奇怪的情况。有什么想法吗 ?

private static string mutex= "ABC";

internal static void Foo(Rpc rpc)
{
    lock (mutex)
    {
        //do something
    }
}

【问题讨论】:

    标签: c# multithreading synchronization mutex


    【解决方案1】:

    这样的字符串(来自代码)可能是“interned”。这意味着“ABC”的所有实例都指向同一个对象。即使跨越AppDomains,您也可以指向同一个对象(感谢 Steven 的提示)。

    如果您有很多字符串互斥体,来自不同的位置,但具有相同的文本,它们都可以锁定在同一个对象上。

    实习生池保存字符串存储。如果将文字字符串常量分配给多个变量,则每个变量都设置为引用实习池中的相同常量,而不是引用具有相同值的多个不同 String 实例。

    最好用:

     private static readonly object mutex = new object();
    

    另外,由于您的字符串不是constreadonly,您可以更改它。所以(理论上)可以锁定你的mutex。将mutex 更改为另一个引用,然后进入临界区,因为锁使用了另一个对象/引用。示例:

    private static string mutex = "1";
    private static string mutex2 = "1";  // for 'lock' mutex2 and mutex are the same
    
    private static void CriticalButFlawedMethod() {
        lock(mutex) {
          mutex += "."; // Hey, now mutex points to another reference/object
          // You are free to re-enter
          ...
        }
    }
    

    【讨论】:

    • 感谢 GvS。但我想知道使用对象类型(我们通常这样做)来锁定使用字符串类型(字符串是可变的)是否有区别。程序有大量线程争用(目前还不确定是否是由于这段代码造成的)。
    • 字符串是不可变的,它们只是看起来是可变的,但是每个不同的字符串都指向另一个引用。锁定它们对我来说看起来很奇怪。你试过 ReaderWriterLock 吗?
    • 字符串是“不可变的”,但引用不是。我添加了一些代码来说明这一点。
    • 我想补充一点,.NET 甚至可以在 AppDomain 之间共享字符串实例(以及 Type 实例),这可能会使 AppDomain 彼此陷入僵局!换句话说,锁定字符串真的很可怕。
    • @Steven - 你对上述关于跨 AppDomains 共享字符串实例的评论有参考吗?
    【解决方案2】:

    为了回答您的问题(正如其他人已经提出的那样),您提供的代码示例存在一些潜在问题:

    private static string mutex= "ABC";
    
    • 变量mutex 不是不可变的。
    • 字符串文字 "ABC" 将在您的应用程序的任何地方引用相同的内部对象引用。

    一般来说,我建议不要锁定字符串。但是,我遇到了一个有用的案例。

    在某些情况下,我维护了一个锁定对象字典,其中的键是我所拥有的某些数据的独特之处。这是一个人为的例子:

    void Main()
    {
        var a = new SomeEntity{ Id = 1 };
        var b = new SomeEntity{ Id = 2 };
    
        Task.Run(() => DoSomething(a));    
        Task.Run(() => DoSomething(a));    
        Task.Run(() => DoSomething(b));    
        Task.Run(() => DoSomething(b));
    }
    
    ConcurrentDictionary<int, object> _locks = new ConcurrentDictionary<int, object>();    
    void DoSomething(SomeEntity entity)
    {   
        var mutex = _locks.GetOrAdd(entity.Id, id => new object());
    
        lock(mutex)
        {
            Console.WriteLine("Inside {0}", entity.Id);
            // do some work
        }
    }   
    

    这样的代码的目标是在实体的Id 的上下文中序列化DoSomething() 的并发调用。缺点是字典。实体越多,它就越大。这也是需要阅读和思考的更多代码。

    我认为 .NET 的字符串实习可以简化事情:

    void Main()
    {
        var a = new SomeEntity{ Id = 1 };
        var b = new SomeEntity{ Id = 2 };
    
        Task.Run(() => DoSomething(a));    
        Task.Run(() => DoSomething(a));    
        Task.Run(() => DoSomething(b));    
        Task.Run(() => DoSomething(b));
    }
    
    void DoSomething(SomeEntity entity)
    {   
        lock(string.Intern("dee9e550-50b5-41ae-af70-f03797ff2a5d:" + entity.Id))
        {
            Console.WriteLine("Inside {0}", entity.Id);
            // do some work
        }
    }
    

    这里的区别是我依靠字符串实习来为每个实体 ID 提供相同的对象引用。这简化了我的代码,因为我不必维护互斥体实例的字典。

    请注意我用作命名空间的硬编码 UUID 字符串。如果我选择在我的应用程序的另一个区域采用相同的方法锁定字符串,这一点很重要。

    根据情况和开发人员对细节的关注程度,锁定字符串可能是个好主意或坏主意。

    【讨论】:

    • 这个答案实际上是我正在寻找的。我需要保证相同的字符串将生成相同的互斥对象,并且字符串实习是一种非常巧妙的机制。我需要它来进行缓存:多个线程可能会从 DB 中检索相同的对象,并且它们必须仅将 1 个条目插入到缓存中,该缓存的键为“Id”。所以现在我只是从这个 Id 和一些永远不会在任何地方使用的任意字符串构造一个用于锁定的字符串(对于那些想要相对安全的人来说,就像 Guid 一样),并在这个锁中进行对象实例化和缓存更新。
    • 很高兴您发现这种方法很有用!
    • 无限增长字典的问题可以通过ConditionalWeakTable来解决。这是一个字典,它允许两个键作为值超出范围,从而允许垃圾收集m
    • @Steven 同意ConditionalWeakTable。这真是一堂方便的课。我在另一个答案stackoverflow.com/a/38226358/64334 中谈到了另一种用法,我最近一直在使用它,并使用一些object 扩展方法将任何东西附加到任何东西上。太棒了。
    • 值得一提的是使用string.Intern 的问题是它也在无限增长。正如MSDN 所述,在公共语言运行时 (CLR) 终止之前,分配给 interned String 对象的内存可能不会被释放 .. 这是您在代码中引入的一种内存泄漏,所以使用时必须格外小心
    【解决方案3】:

    如果您需要锁定字符串,您可以创建一个对象,将字符串与您可以锁定的对象配对。

    class LockableString
    {
         public string _String; 
         public object MyLock;  //Provide a lock to the data in.
    
         public LockableString()
         {
              MyLock = new object();
         }
    }
    

    【讨论】:

      【解决方案4】:

      我的 2 美分:

      1. ConcurrentDictionary 比内部字符串快 1.5 倍。我做过一次基准测试。

      2. 要解决“不断增长的字典”问题,您可以使用信号量字典而不是对象字典。 AKA 使用 ConcurrentDictionary&lt;string, SemaphoreSlim&gt; 而不是 &lt;string, object&gt;。与lock 语句不同,信号量可以跟踪锁定它们的线程数。一旦所有的锁都被释放 - 你可以从字典中删除它。请参阅此问题以获取此类解决方案:Asynchronous locking based on a key

      3. 信号量更好,因为您甚至可以控制并发级别。就像,而不是“限制为一个并发运行” - 您可以“限制为 5 个并发运行”。很棒的免费奖金不是吗?我必须编写一个电子邮件服务,该服务需要限制与服务器的并发连接数 - 这非常方便。

      【讨论】:

        【解决方案5】:

        我想如果生成的字符串很多并且都是唯一的,那么锁定实习字符串可能会导致内存膨胀。另一种应该提高内存效率并解决直接死锁问题的方法是

        // Returns an Object to Lock with based on a string Value
        private static readonly ConditionalWeakTable<string, object> _weakTable = new ConditionalWeakTable<string, object>();
        public static object GetLock(string value)
        {
            if (value == null) throw new ArgumentNullException(nameof(value));
            return _weakTable.GetOrCreateValue(value.ToLower());
        }
        

        【讨论】:

        • 这不适用于动态字符串,因为那时字符串没有被保留。它为相同的字符串生成新的锁定对象。 GetOrCreateValue 生成一个 ReferenceEquals,因此在使用字符串锁定时存在同样的问题。没有帮助:(
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-09-25
        • 2011-10-12
        • 2014-05-02
        • 2011-01-11
        • 1970-01-01
        相关资源
        最近更新 更多