使用字符串作为锁来进行线程同步

34

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

private static string mutex= "ABC";

internal static void Foo(Rpc rpc)
{
    lock (mutex)
    {
        //do something
    }
}
5个回答

42

像代码中这样的字符串可以被“内部化”。这意味着所有的“ABC”实例都指向同一个对象。甚至在跨AppDomain时,也可以指向同一个对象(感谢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。但我在想,使用对象类型(通常我们这样做)来锁定与使用字符串类型(字符串是可变的)是否有区别。程序具有大量线程争用(尚不确定是否由于此代码片段)。 - Illuminati
字符串不可变,它们看起来可变,但每个不同的字符串都指向另一个引用。在它们上面进行锁定对我来说看起来很奇怪。你尝试过ReaderWriterLock吗? - GvS
抱歉,我想说的是“字符串是不可变的” :) ..我没有尝试使用ReaderWriterLock..只是试图找出导致问题的确切代码部分。由于数据不足,很难在本地环境中重新创建这种情况。但我会花些时间尝试重新制造这个问题。再次感谢。 - Illuminati
1
字符串是“不可变的”,但引用是可变的。我添加了一些代码来说明这一点。 - GvS
8
我想补充一点,.NET甚至允许在应用程序域之间共享字符串实例(以及“Type”实例),这可能会导致应用程序域彼此死锁!换句话说,对字符串进行锁定真的很可怕。 - Steven
1
@Steven - 你有在 AppDomains 之间共享字符串实例的上述评论的参考吗? - Armbrat

40
回答你的问题(就像其他一些人已经做过的那样),你提供的代码示例存在一些潜在问题:
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字符串作为命名空间。如果我选择在应用程序的另一个领域中采用锁定字符串的方法,这一点很重要。

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


6
这个答案实际上是我在寻找的。我需要保证相同的字符串将生成相同的互斥对象,而字符串内部化是一种非常巧妙的机制来实现这一点。我需要它用于缓存目的:多个线程可能从数据库检索到相同的对象,他们必须只向 "Id" 键的缓存中插入一个条目。所以现在我只需从此 Id 构建一个用于锁定的字符串和一些绝对不会在任何地方使用的任意字符串(例如 Guid),并在此锁定中进行对象实例化和缓存更新。 - evilkos
我很高兴你发现这种方法有用! - Ronnie Overby
1
使用ConditionalWeakTable可以解决无限增长字典的问题。这是一个允许键和值超出作用域的字典,这使得垃圾回收变得更加容易。 - Steven
2
@Steven 对于 ConditionalWeakTable 的使用我也赞同。这是一个非常方便的类。我在另一个答案中谈到了另一种用法 https://dev59.com/N2445IYBdhLWcg3wLXWh#38226358最近我一直在使用它,结合一些 object 扩展方法来将任何东西附加到任何东西上。这非常棒。 - Ronnie Overby
7
值得一提的是,使用string.Intern的问题在于它还会无限增长。根据MSDN的说明,“为强名称程序集保留的内存可能会不会被释放,直到公共语言运行时 (CLR) 终止。” 这是一种内存泄漏,如果要使用它,则必须格外小心。 - AmitE
显示剩余2条评论

1

我的看法:

  1. ConcurrentDictionary 比 interned strings 快 1.5 倍。我曾经做过基准测试。

  2. 为了解决“不断增长的字典”问题,你可以使用 信号量字典 而不是对象字典。即使用 ConcurrentDictionary<string, SemaphoreSlim> 代替 <string, object>。与 lock 语句不同,信号量可以跟踪锁定它们的线程数量。一旦所有锁都被释放,就可以从字典中删除它。查看这个问题以获取类似的解决方案:基于键的异步锁定

  3. 信号量更好,因为 你甚至可以控制并发级别。例如,“限制为一个并发运行”可以改为“限制为 5 个并发运行”。非常棒的免费奖励,不是吗?我曾经编写过一个需要限制连接到服务器的并发连接数的电子邮件服务 - 这非常有用。


1
如果您需要锁定一个字符串,您可以创建一个将该字符串与一个可锁定的对象配对的对象。
class LockableString
{
     public string _String; 
     public object MyLock;  //Provide a lock to the data in.

     public LockableString()
     {
          MyLock = new object();
     }
}

0

我想象一下,如果生成的字符串很多且都是唯一的,那么在interned字符串上锁定可能会导致内存膨胀。另一种更节省内存并解决即时死锁问题的方法是

// 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());
}

1
这对于动态字符串无效,因为字符串没有被内部化。它会为相同的字符串产生新的锁对象。GetOrCreateValue使用ReferenceEquals,因此在使用字符串锁时存在相同的问题。没有帮助 :( - b.kiener

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接