深入浅出多线程系列之四:简单的同步 lock

1: 考虑下下面的代码:

class  ThreadUnsafe
    {
        
static   int  _val1  =   1 , _val2  =   1 ;

        
internal   static   void  Go()
        {
            
if  (_val2  !=   0 )
            {
                Console.WriteLine(_val1 
/ _val2);
            }
            _val2 
=   0 ;
        }
    }

 

这段代码是非线程安全的,假设有两个线程A,BA,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   readonly   object  _locker  =   new   object ();
        
static   int  _val1  =   1 , _val2  =   1 ;

        
internal   static   void  Go()
        {
            
lock  (_locker)
            {
                
if  (_val2  !=   0 )
                {
                    Console.WriteLine(_val1 
/  _val2);
                }
                _val2 
=   0 ;
            }
        }

此时线程AB都只能有一个可以获得_locker锁,所以只能有一个线程来执行lock块的代码。

C#Lock关键字实际上是Monitor.Enter,Monitor.Exit的缩写。例如上面的代码和下面的等价。

            Monitor.Enter(_locker);
            
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

我们可以将上面的代码改成下面的版本。

            bool  lockTaken  =   false ;
            
try
            {
                Monitor.Enter(_locker, 
ref  lockTaken);
                
//  Do something..
            }
            
finally
                
if (lockTaken) {
                    Monitor.Exit(_locker);
                }
            }

Monitor也提供了TryEnter方法,并且可以传递一个超时时间。如果方法返回True,则代表获取了锁,否则为false

 

2:选择同步对象。

Monitor.Enter方法的参数是一个object类型,所以任何对象都可以是同步对象,考虑下下面的代码:

  1. int i=5; lock(i){}         // 锁定值类型
  2. lock(this){}           // 锁定 this 对象
  3. lock(typeof(Product)){}      // 锁定 type 对象。
  4. string str="dddd"; lock(str){}   // 锁定字符串

1:锁定值类型会将值类型进行装箱,所以Monitor.Enter进入的是一个对象,但是Monitor.Exit()退出的是另一个不同的对象。

23:锁定thistype对象,会导致无法控制锁的逻辑,并且它很难保证不死锁和频繁的阻塞,在相同进程中锁定type对象会穿越应用程序域。

4:由于字符串驻留机制,所以也不要锁定string,关于这点,请大家去逛一逛老A的博客。

 

3:嵌套锁:

同一个线程可以多次锁定同一对象。例如

lock (locker)
    
lock (locker)
        
lock (locker)
        {
            
//  do something
     }

或者是:

Monitor.Enter(locker); Monitor.Enter(locker); Monitor.Enter(locker);
// Do something.
Monitor.Exit(locker); Monitor.Exit(locker); Monitor.Exit(locker);

 

当一个线程使用一个锁调用另一方法的时候,嵌套锁就非常的有用。例如:

       static   readonly   object  _locker  =   new   object ();

        
static   void  Main()
        {
            
lock  (_locker)
            { 
                AnotherMethod();
            }
        }

        
static   void  AnotherMethod()
        {
            
lock  (_locker){  // dosomething;}
        }

 

4:死锁:

先看下面的代码:

        static   object  locker1  =   new   object ();
        
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

 

你可能感兴趣的:(Lock)