使用string作为锁来进行线程同步

时间:2010-11-16 10:05:27

标签: c# multithreading synchronization mutex

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

private static string mutex= "ABC";

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

5 个答案:

答案 0 :(得分:36)

这样的字符串(来自代码)可以是“interned”。这意味着“ABC”的所有实例都指向同一个对象。即使在AppDomain之内,你也可以指向同一个对象(对于小费来说是史蒂文)。

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

  

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

最好使用:

 private static readonly object mutex = new object();

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

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
      ...
    }
}

答案 1 :(得分:24)

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

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
    }
}   

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

我认为.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字符串。如果我选择在我的应用程序的另一个区域中采用相同的方法锁定字符串,这很重要。

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

答案 2 :(得分:1)

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

class LockableString
{
     public string _String; 
     public object MyLock;  //Provide a lock to the data in.

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

答案 3 :(得分:0)

我想如果生成的字符串很多并且都是唯一的,则对内联字符串的锁定可能导致内存膨胀。应该更有效地利用内存并解决即时死锁问题的另一种方法是

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

答案 4 :(得分:0)

我的 2 美分:

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

  2. 要解决“不断增长的字典”问题,您可以使用信号量字典代替对象字典。也就是使用 ConcurrentDictionary<string, SemaphoreSlim> 而不是 <string, object>。与 lock 语句不同,信号量可以跟踪锁定了多少线程。一旦所有的锁都被释放了——你可以从字典中删除它。请参阅此问题以获取类似解决方案:Asynchronous locking based on a key

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