如果你编写线程安全代码时为了省事儿直接 lock(this)
,或者早已听说不应该 lock(this)
,只是不知道原因,那么阅读本文可以帮助你了解原因。
原因
不应该 lock(this)
是因为你永远不知道别人会如何使用你的对象,永远不知道别人会在哪里加锁。于是稍不注意就可能死锁!
实例
看看下面的两段代码。
第一段是定义好的一个类,其中某个方法为了线程安全加了锁,但加锁的是 this
对象。
1
2
3
4
5
6
7
8
9
10
public class Foo
{
public void DoSafety()
{
lock (this)
{
// 执行一些线程安全的事情。
}
}
}
第二段代码使用了这个类的一个实例。为了响应放到了后台线程中,但为了线程安全,加了锁。
1
2
3
4
5
6
7
8
9
10
11
12
public class Bar
{
private readonly Foo _foo = new Foo();
public async void DouB_Walterlv()
{
lock (_foo)
{
await Task.Run(() => _foo.DoSafety());
}
}
}
仔细看看这段代码,如果 DouB_Walterlv
方法执行,会发生什么?
—— 死锁
在 DouB_Walterlv
方法中完全看不出来为什么死锁,只能进入到 DoSafety
中才发现试图 lock
的 this
对象刚刚在另一个线程被 lock (_foo)
了。
扩展
从以上的例子可以看出,不止是 lock (this)
会出现“难以捉摸”的死锁问题,lock
任何公开对象都会这样。
lock 公开的属性
1
2
3
4
public class Foo
{
public object SyncRoot { get; } = new object();
}
只要在 A 处 lock
这个对象的同时,在另一个线程调用了同样 lock
这个对象的 B 处的代码,必然死锁。
如果你试图实现某些接口中的 SyncRoot
属性,却遇到了上述矛盾(这样的写法不安全),那么可以阅读我的另一篇博客了解如何实现这样的“有问题”的接口:
lock 字符串
你可以定义一个私有的字符串,但你永远不知道这个字符串是否与其他字符串是同一个实例。因此这也是不安全的。
- .NET/C# 的字符串暂存池 - walterlv
- .NET/C# 编译期间能确定的相同字符串,在运行期间是相同的实例 - walterlv
- .NET/C# 编译期能确定的字符串会在字符串暂存池中不会被 GC 垃圾回收掉 - walterlv
lock 其他任何可能被其他对象获取的公开对象
比如 Type
对象,比如其他公共静态对象。
结论
所以,一旦你决定 lock
,那么这个对象请做成 private
。
本文会经常更新,请阅读原文: https://blog.walterlv.com/post/why-making-the-sync-root-public-is-dangerous.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 吕毅 (包含链接: https://blog.walterlv.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 ([email protected]) 。