在查看某些遗留应用程序代码时,我注意到它使用字符串对象来进行线程同步。我正试图解决该程序中的一些线程争用问题,并想知道这是否会导致一些奇怪的情况。你有什么想法吗?
private static string mutex= "ABC";
internal static void Foo(Rpc rpc)
{
lock (mutex)
{
//do something
}
}
在查看某些遗留应用程序代码时,我注意到它使用字符串对象来进行线程同步。我正试图解决该程序中的一些线程争用问题,并想知道这是否会导致一些奇怪的情况。你有什么想法吗?
private static string mutex= "ABC";
internal static void Foo(Rpc rpc)
{
lock (mutex)
{
//do something
}
}
像代码中这样的字符串可以被“内部化”。这意味着所有的“ABC”实例都指向同一个对象。甚至在跨AppDomain时,也可以指向同一个对象(感谢Steven的提示)。
如果您有许多来自不同位置但具有相同文本的字符串互斥锁,则它们都可以锁定同一个对象。
内部池可节省字符串存储空间。如果将文字字符串常量分配给多个变量,则每个变量都被设置为引用内部池中的相同常量,而不是引用具有相同值的几个不同String实例。
更好的方法是使用:
private static readonly object mutex = new object();
另外,由于您的字符串不是const
或readonly
,因此可以更改它。 因此(理论上)可能锁定您的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
...
}
}
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字符串作为命名空间。如果我选择在应用程序的另一个领域中采用锁定字符串的方法,这一点很重要。
根据情况和开发人员对细节的关注度,锁定字符串可能是一个好主意或者是一个坏主意。
ConditionalWeakTable
可以解决无限增长字典的问题。这是一个允许键和值超出作用域的字典,这使得垃圾回收变得更加容易。 - StevenConditionalWeakTable
的使用我也赞同。这是一个非常方便的类。我在另一个答案中谈到了另一种用法 https://dev59.com/N2445IYBdhLWcg3wLXWh#38226358最近我一直在使用它,结合一些 object
扩展方法来将任何东西附加到任何东西上。这非常棒。 - Ronnie Overbystring.Intern
的问题在于它还会无限增长。根据MSDN的说明,“为强名称程序集保留的内存可能会不会被释放,直到公共语言运行时 (CLR) 终止。” 这是一种内存泄漏,如果要使用它,则必须格外小心。 - AmitE我的看法:
ConcurrentDictionary
比 interned strings 快 1.5 倍。我曾经做过基准测试。
为了解决“不断增长的字典”问题,你可以使用 信号量字典 而不是对象字典。即使用 ConcurrentDictionary<string, SemaphoreSlim>
代替 <string, object>
。与 lock
语句不同,信号量可以跟踪锁定它们的线程数量。一旦所有锁都被释放,就可以从字典中删除它。查看这个问题以获取类似的解决方案:基于键的异步锁定
信号量更好,因为 你甚至可以控制并发级别。例如,“限制为一个并发运行”可以改为“限制为 5 个并发运行”。非常棒的免费奖励,不是吗?我曾经编写过一个需要限制连接到服务器的并发连接数的电子邮件服务 - 这非常有用。
class LockableString
{
public string _String;
public object MyLock; //Provide a lock to the data in.
public LockableString()
{
MyLock = new object();
}
}
我想象一下,如果生成的字符串很多且都是唯一的,那么在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());
}