-
C#教程之C#中lock死锁实例教程
在c#中有个关键字lock,它的作用是锁定某一代码块,让同一时间只有一个线程访问该代码块,本文就来谈谈lock关键字的原理和其中应注意的几个问题:
lock的使用原型是:
1
2
3
4
|
lock (X) { //需要锁定的代码.... } |
首先要明白为什么上面这段话能够锁定代码,其中的奥妙就是X这个对象,事实上X是任意一种引用类型,它在这儿起的作用就是任何线程执行到lock(X)时候,X需要独享才能运行下面的代码,若假定现在有3个线程A,B,C都执行到了lock(X)而ABC因为此时都占有X,这时ABC就要停下来排个队,一个一个使用X,从而起到在下面的代码块内只有一个线程在运行(因为此时只有一个线程独享X,其余两个在排队),所以这个X必须是所有要执行临界区域代码进程必须共有的一个资源,从而起到抑制线程的作用。
下面再来谈谈lock使用中会遇到和注意的问题,lock最需要注意的一个问题就是线程死锁!
在MSDN上列出了3个典型问题:
通常,应避免锁定 public 类型,否则实例将超出代码的控制范围。常见的结构 lock (this)、lock (typeof (MyType)) 和 lock ("myLock") 违反此准则:
如果实例可以被公共访问,将出现 lock (this) 问题。
如果 MyType 可以被公共访问,将出现 lock (typeof (MyType)) 问题。
由于进程中使用同一字符串的任何其他代码将共享同一个锁,所以出现 lock(“myLock”) 问题。
最佳做法是定义 private 对象来锁定, 或 private shared 对象变量来保护所有实例所共有的数据。
(1)lock (this) 问题:
假定有两个类:
1
2
|
class A{} class B{} |
有两个公共对象:
1
2
|
A a= new A(); B b= new B(); |
首先在A中若有一函数内的代码需要锁定:
代码1:
1
2
3
4
5
6
7
8
|
lock ( this ) //this在这里就是a { //.... lock (b) { //...... } } |
然而此时B中某函数也有如下代码需要锁定:
代码2:
1
2
3
4
5
6
7
8
|
lock ( this ) //this在这里就是b { //.... lock (a) { //...... } } |
设想一下上面两段代码在两个线程下同时执行会有什么后果?
结果就是,代码1执行到lock(this)后a被锁定,代码2执行到lock(this)后b被锁定,然后代码1需求b,代码2需求a,此时两个需求都被相互占有出现僵持状态,程序死锁了。
(2)lock(typeof (MyType))问题:
假定有两个公共变量:
1
|
int a; float b; |
下面看如下代码
代码3:
1
2
3
4
5
6
7
8
|
lock ( typeof (a)) //typeof(a)就是System.type.Int类型 { //.... lock ( typeof (b)) { //...... } } |
又有如下代码:
代码4:
1
2
3
4
5
6
7
8
|
lock ( typeof (b)) //typeof(b)就是System.type.Float类型 { //.... lock ( typeof (a)) { //...... } } |
若有两个进程分别同时进入上面两个代码外层的lock,就分别锁定了System.type.Int和System.type.Float,而马上它们又需求System.type.Float和System.type.Int,彼此相互占有,彼此僵持,程序进入死锁状态!
(3)字符串问题 :
在阐述这个问题之前,有一个知识大家必须知道:C#中字符串被公共语言运行库 (CLR)“暂留”。这意味着整个程序中任何给定字符串都只有一个实例,就是这同一个对象表示了所有运行的应用程序域的所有线程中的该文本。因此,只要在应用程序进程中的任何位置处具有相同内容的字符串上放置了锁,就将锁定应用程序中该字符串的所有实例。
言下之意就是假定有两个类分别有两个字符串:
1
2
3
4
5
6
7
8
9
10
11
|
class A { string a= "abc" ; string b= "def" ; } class c { string c= "abc" ; string d= "def" ; } |
事实上a和c引用的是同一个字符串"abc",b和d引用的是同一个字符串"def"
现在如果在两个类中有如下代码
在类A中有代码5:
1
2
3
4
5
6
7
8
|
lock (b) //b是"def" { //.... lock (a) //a是"abc" { //...... } } |
在类B中有代码6:
1
2
3
4
5
6
7
8
|
lock (c) //c是"abc" { //.... lock (d) //d是"def" { //...... } } |
那么代码5和代码6同时有两个线程执行结果可想而知:在两个线程执行到外层lock代码时"def"和"abc"被锁定。接着他们在内部lock处同时需求"abc"和"def",而此时两个字符串被两个进程彼此占有,程序又死锁了!所以MSDN说:锁定字符串尤其危险!最好不要使用!
MSDN最后说了:最佳做法是定义 private 对象来锁定, 或 private shared 对象变量来保护所有实例所共有的数据。
在个人看来,也不是绝对安全,这里就举出一个例子:
假定有一个类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
class A { private Object a= new Object(); private Object b= new Object(); public void x() { lock (a) { //..... lock (b) { //.... } } } public void y() { lock (b) { //..... lock (a) { //.... } } } } |
现在假定有两个线程同时执行函数x()和y();结果private对象a和b分别在外层lock锁定,接着两个线程在内部又立马需求b和a,a,b彼此占有又彼此需求,程序死锁。
所以具体要看情况而定,但是定义 private 对象来锁定至少可以降低风险。
希望本文所述C#中lock的应用对大家的C#程序设计有所帮助。