深入浅出多线程系列之四:简单的同步 lock
1: 考虑下下面的代码:
{
static int _val1 = 1, _val2 = 1;
internal static void Go()
{
if (_val2 != 0)
{
Console.WriteLine(_val1 /_val2);
}
_val2 = 0;
}
}
这段代码是非线程安全的,假设有两个线程A,B,A,B都执行到了Go方法的if判断中,假设_val2=1.所以两个线程A,B都通过if判断,
A执行了Console.WriteLine方法,然后退出if语句,执行_val2=0,此时_val2=0.
但是此时线程B才刚刚执行到Console.WriteLine方法,而此时_val2=0.所以你有可能会得到一个divide by zero 的异常。
为了保证线程安全,我们可以使用Lock关键字,例如:
static int _val1 = 1, _val2 = 1;
internal static void Go()
{
lock (_locker)
{
if (_val2 != 0)
{
Console.WriteLine(_val1 / _val2);
}
_val2 = 0;
}
}
此时线程A,B都只能有一个可以获得_locker锁,所以只能有一个线程来执行lock块的代码。
C#的Lock关键字实际上是Monitor.Enter,和Monitor.Exit的缩写。例如上面的代码和下面的等价。
try
{
if (_val2 != 0)
{
Console.WriteLine(_val1 / _val2);
}
_val2 = 0;
}
finally { Monitor.Exit(_locker); }
如果在调用Monitor.Exit之前没有调用Monitor.Enter,则会抛出一个异常。
不知道大家注意到没有,在Monitor.Enter 和 Try 方法之间可能会抛出异常。
例如在线程上调用Abort,或者是OutOfMemoryException。
为了解决这个问题CLR 4.0提供了Monitor.Enter的重载,增加了lockTaken 字段,当Monitor.Enter成功获取锁之后,lockTaken就是True,否则为False。
我们可以将上面的代码改成下面的版本。
try
{
Monitor.Enter(_locker, ref lockTaken);
// Do something..
}
finally{
if(lockTaken) {
Monitor.Exit(_locker);
}
}
Monitor也提供了TryEnter方法,并且可以传递一个超时时间。如果方法返回True,则代表获取了锁,否则为false。
2:选择同步对象。
Monitor.Enter方法的参数是一个object类型,所以任何对象都可以是同步对象,考虑下下面的代码:
-
int i=5; lock(i){} //锁定值类型
-
lock(this){} //锁定this对象
-
lock(typeof(Product)){} //锁定type对象。
-
string str="dddd"; lock(str){} //锁定字符串
1:锁定值类型会将值类型进行装箱,所以Monitor.Enter进入的是一个对象,但是Monitor.Exit()退出的是另一个不同的对象。
2,3:锁定this和type对象,会导致无法控制锁的逻辑,并且它很难保证不死锁和频繁的阻塞,在相同进程中锁定type对象会穿越应用程序域。
4:由于字符串驻留机制,所以也不要锁定string,关于这点,请大家去逛一逛老A的博客。
3:嵌套锁:
同一个线程可以多次锁定同一对象。例如
lock(locker)
lock(locker)
{
// do something
}
或者是:
//Do something.
Monitor.Exit(locker); Monitor.Exit(locker); Monitor.Exit(locker);
当一个线程使用一个锁调用另一方法的时候,嵌套锁就非常的有用。例如:
static void Main()
{
lock (_locker)
{
AnotherMethod();
}
}
static void AnotherMethod()
{
lock (_locker){ //dosomething;}
}
4:死锁:
先看下面的代码:
static object locker2 = new object();
public static void MainThread()
{
new Thread(() =>
{
lock (locker1) //获取锁locker1
{
Thread.Sleep(1000);
lock (locker2) //尝试获取locker2
{
Console.WriteLine("locker1,locker2");
}
}
}).Start();
lock (locker2) //获取锁locker2
{
Thread.Sleep(1000);
lock (locker1) //尝试获取locker1
{
Console.WriteLine("locker2,locker1");
}
}
}
在这里
主线程先获取locker2的锁,然后sleep,接着尝试获取locker1的锁。
副线程先获取locker1的锁,然后sleep,接着尝试获取locker2的锁。
程序进入了死锁状态,两个线程都在等待对方释放自己等待的锁。
CLR作为一个独立宿主环境,它不像SQL Server一样,它没有自动检测死锁机制,也不会结束一个线程来破坏死锁。死锁的线程会导致部分线程无限的等待。
下篇文章会介绍一些其他同步构造。
参考资料:
http://www.albahari.com/threading/
CLR Via C# 3.0