多线程中lock用法

我对lock的用法很模糊,所以专门研究一下lock用法。

lock主要功能:

    1、lock(typeof(类名)),保护一个静态变量(如果互斥代码段在一个静态方法内部)。

    2、lock(this),主要是保护一个类的实例。

    3、lock(object),通过某个对象锁定一段代码。

 请看网上的说明: (原文地址:http://blog.csdn.net/FantasiaX/article/details/561797)

1.使用lock关键字的第一个目的:保证共享资源的安全
   
当多个线程共享一个资源的时候,常常会产生协同问题,这样的协同问题往往是由于时间延迟引起的。拿银行的ATM机举例,如果里面有可用资金5000元,每个人每次可以取50200元,现在有100个人来取钱。假设一个人取钱的时候,ATM机与银行数据库的沟通时间为10秒,那么在与总行计算机沟通完毕之前(也就是把你取的钱从可用资金上扣除之前),ATM机不能再接受别一个人的请求——也就是被“锁定”。这也就是lock关键字得名的原因。
如果不“锁定”ATM会出现什么情况呢?假设ATM里只剩下100元了,后面还有很多人等着取钱,一个人取80ATM验证80<100成立,于是吐出80,这时需要10秒钟与银行总机通过网络沟通(网络,特别是为了保证安全的网络,总是有延迟的),由于没有锁定ATM,后面的客户也打算取80……戏剧性的一幕出现了:ATM又吐出来80!因为这时候它仍然认为自己肚子里有100元!下面的程序就是这个例子的完整实现。
        
这个例子同时也展现了lock关键第的第一种用法:针对由静态方法构造的线程,由于线程所执行的方法并不具有类的实例作为载体,所以,“上锁”的时候,只能是锁这个静态方法所在的类——lock (typeof(ATM))
using System;
using System.Threading;

namespace LockSample
{
     class ATM
     {
         static int remain = 5000;//可用金额

         public static void GiveOutMoney(int money)
         {
              lock (typeof(ATM))//核心代码!注释掉这句,会得到红色警报
              {
                   if (remain >= money)
                   {
                       Thread.Sleep(100);//模拟时间延迟
                       remain -= money;
                   }
              }
              if (remain >= 0)
              {
                   Console.ForegroundColor = ConsoleColor.Green;
                   Console.WriteLine("{0}$ /t in ATM.", remain);
              }
              else
              {
                   Console.ForegroundColor = ConsoleColor.Red;
                   Console.WriteLine("{0}$ /t remained.", remain);
              }
         }
     }

     class Boy
     {
         Random want = new Random();
         int money;

         public void TakeMoney()
         {
                   money = want.Next(50200);
                   ATM.GiveOutMoney(money);
         }
     }
    
     class Program
     {
         static void Main(string[] args)
         {
              Boy[] Boys = new Boy[100];
              Thread[] Threads = new Thread[100];
              for (int i = 0; i < 100; i++)
              {
                   Boys[i] = new Boy();
                   Threads[i] = new Thread(new ThreadStart(Boys[i].TakeMoney));
                   Threads[i].Name = "Boy" + i.ToString();
                   Threads[i].Start();
              }
         }
     }
}
2.使用lock关键字的第二个目的:保证线程执行的顺序合理
        
回想上面的例子:取钱这件事情基本上可以认为是一个操作就能完成,而很多事情并不是一步就能完成的,特别是如果每一步都与某个共享资源挂钩时,如果在一件事情完成(比如十个操作步骤)之前不把资源锁进来,那么N多线程乱用资源,肯定会混乱不堪的。相反,如果我们在一套完整操作完成之前能够锁定资源(保证使用者的“独占性”),那么想使用资源的N多线程也就变得井然有序了。
        
狗 年快到了,让我们来看看我们的狗妈妈是怎样照顾她的小宝贝的。狗妈妈“花花”有三个小宝贝,它们的身体状况不太相同:壮壮很壮,总是抢别人的奶吃;灵灵体 格一般,抢不到先也不会饿着;笨笨就比较笨了,身体弱,总是喝不着奶。这一天,狗妈妈决定改善一下给小宝贝们喂奶的方法——由原来的哄抢方式改为一狗喂十 口,先喂笨笨,然后是灵灵,最后才是壮壮……在一只小狗狗吮完十口之前,别的小狗狗不许来捣蛋!OK,让我们看下面的代码:
        
注意,这段代码展示了lock的第二种用法——针对由实例方法构造的线程,lock将锁住这个方法的实例载体,也就是使用了——lock (this)
 
using System;
using System.Threading;

namespace LockSample2
{
     class DogMother
     {
         //喂一口奶
         void Feed()
         {
              //Console.ForegroundColor = ConsoleColor.Yellow;
              
//Console.WriteLine("Puzi...zi...");
              
//Console.ForegroundColor = ConsoleColor.White;
              Thread.Sleep(100);//喂一口奶的时间延迟
         }
         //每只狗狗喂口奶
         public void FeedOneSmallDog()
         {
              //因为用到了实例方法,所以要锁this,this是本类运行时的实例
              
//注释掉下面一行,回到哄抢方式,线程的优先级将产生效果
              lock (this)
              {
                   for (int i = 1; i <= 10; i++)
                   {
                       this.Feed();
                       Console.WriteLine(Thread.CurrentThread.Name.ToString() + " sucked {0} time.", i);
                   }
              }
         }
     }

     class Program
     {
         static void Main(string[] args)
         {
              DogMother huahua = new DogMother();

              Thread DogStrong = new Thread(new ThreadStart(huahua.FeedOneSmallDog));
              DogStrong.Name = "Strong small Dog";
              DogStrong.Priority = ThreadPriority.AboveNormal;

              Thread DogNormal = new Thread(new ThreadStart(huahua.FeedOneSmallDog));
              DogNormal.Name = "Normal small Dog";
              DogNormal.Priority = ThreadPriority.Normal;

              Thread DogWeak = new Thread(new ThreadStart(huahua.FeedOneSmallDog));
              DogWeak.Name = "Weak small Dog";
              DogWeak.Priority = ThreadPriority.BelowNormal;

              //由于lock的使用,线程的优先级就没有效果了,保证了顺序的合理性
              
//注释掉lock句后,线程的优先级将再次显现效果
              DogWeak.Start();
              DogNormal.Start();
              DogStrong.Start();
         }
     }
}

 另外有一些注意事项:(原文:http://www.cnblogs.com/chengqscjh/archive/2010/12/12/1903784.html)

1.1    定义:

1.1.1   临界区(Critical Section

  临界区是一段在同一时候只被一个线程进入/执行的代码块。 

  

1.1.2   lock 关键字

将语句块标记为临界区,方法是获取给定对象的互斥锁,执行语句,然后释放该锁。此语句的形式如下:


Object thisLock = new Object();
lock (thisLock)
{
    // Critical code section
}

lock 确保当一个线程位于代码的临界区时,另一个线程不进入临界区。如果其他线程试图进入锁定的代码,则它将一直等待(即被阻止),直到该对象被释放。

1.2    Lock用法实例

下例使用线程和 lock。只要 lock 语句存在,语句块就是临界区并且 balance 永远不会是负数。


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace TestLock
{
    /// <summary>
    
/// 
    
/// </summary>
    public class Account
    {
        private static readonly Object thisLock = new Object();
        int balance;

        Random r = new Random();

        public Account(int initial)
        {
            balance = initial;
        }

        /// <summary>
        
/// 
        
/// </summary>
        
/// <param name="amount"></param>
        
/// <returns></returns>
        int Withdraw(int amount)
        {
            // This condition will never be true unless the lock statement
            
// is commented out:
            if (balance < 0)
            {
                throw new Exception("Negative Balance");
            }

            // Comment out the next line to see the effect of leaving out 
            
// the lock keyword:
            lock (thisLock)
            {
                if (balance >= amount)
                {
                    Console.WriteLine("------------------:" + System.Threading.Thread.CurrentThread.Name + "-------------");

                    Console.WriteLine("Balance before Withdrawal :  " + balance);
                    Console.WriteLine("Amount to Withdraw        : -" + amount);
                    balance = balance - amount;
                    Console.WriteLine("Balance after Withdrawal  :  " + balance);
                    return amount;
                }
                else
                {
                    return 0// transaction rejected
                }
            }
        }

        public void DoTransactions()
        {
            for (int i = 0; i < 100; i++)
            {
                Withdraw(r.Next(1100));
            }
        }

    }
}

执行结果如下图所示:

1.3    Lock的使用准则

    Lock 调用块开始位置的 Enter 和块结束位置的 Exit。
    通常,应避免锁定public 类型,否则实例将超出代码的控制范围。常见的结构 lock (this)、lock (typeof (MyType)) 和 lock ("myLock") 违反此准则:
  • 如果实例可以被公共访问,将出现 lock (this) 问题。
  • 如果 MyType 可以被公共访问,将出现 lock (typeof (MyType)) 问题。
  • 由于进程中使用同一字符串的任何其他代码将共享同一个锁,所以出现lock(“myLock”) 问题。
    最佳做法是定义private 对象来锁定, 或private static 对象变量来保护所有实例所共有的数据。
    针对上述三点原则我们用具体的代码来加深理解

1.3.1   为什么不要Lock值类型

    为什么不能lock值类型,比如lock(1)呢?让我们对上面的实例代码进行修改,lock(thisLock)改为Lock(1),编译器时会出现int不是Lock语句要求的引用对象

    lock本质上Monitor.EnterMonitor.Enter会使值类型装箱,每次lock的是装箱后的对象。lock其实是类似编译器的语法糖,因此编译器直接限制住不能lock值类型。

1.3.2  锁定lock((object)1)可以吗?

    我们还是使用上面的实例代码来进行验证,lock(thisLock)改为Lock((object)1),这个时候编译倒是没什么问题了,但是运行结果中会出现balance < 0,最后抛出异常.如下图所示:


    说明代码块并没有锁.究其原因是因为Lock(对象)中,对象为引用类型,其是通过判断 object.ReferenceEquals((object)1, (object)1)始终返回false(因为每次装箱后都是不同对象),也就是说每次都会判断成未申请互斥锁,这样在同一时间,别的线程照样能够访问里 面的代码,达不到同步的效果。

1.3.3 为什么不要Lock(this)?(原文:http://blog.csdn.net/liang4571231/article/details/6748823)

    我们先来看如下一段代码:

using System;
using System.Threading;

namespace Namespace1
{
    class C1
     {
        private bool deadlocked = true;

        //这个方法用到了lock,我们希望lock的代码在同一时刻只能由一个线程访问
        public void LockMe(object o)
         {
            lock (this)
             {
                while(deadlocked)
                 {
                     deadlocked = (bool)o;
                     Console.WriteLine("Foo: I am locked :(");
                     Thread.Sleep(500);
                 }
             }
         }

        //所有线程都可以同时访问的方法
        public void DoNotLockMe()
         {
             Console.WriteLine("I am not locked :)");
         }
     }

    class Program
     {
        static void Main(string[] args)
         {
             C1 c1 = new C1();

            //在t1线程中调用LockMe,并将deadlock设为true(将出现死锁)
             Thread t1 = new Thread(c1.LockMe);
             t1.Start(true);
             Thread.Sleep(100);

            //在主线程中lock c1
            lock (c1)
             {
                //调用没有被lock的方法
                 c1.DoNotLockMe();
                //调用被lock的方法,并试图将deadlock解除
                 c1.LockMe(false);
             }
         }
     }
    在t1线程中,LockMe调用了lock(this), 也就是Main函数中的c1,这时候在主线程中调用lock(c1)时,必须要等待t1中的lock块执行完毕之后才能访问c1,即所有c1相关的操作都无法完成,于是我们看到连c1.DoNotLockMe()都没有执行。

     C1的代码稍作改动:

class C1
     {
        private bool deadlocked = true;
        private object locker = new object();

        //这个方法用到了lock,我们希望lock的代码在同一时刻只能由一个线程访问
        public void LockMe(object o)
         {
            lock (locker)
             {
                while(deadlocked)
                 {
                     deadlocked = (bool)o;
                     Console.WriteLine("Foo: I am locked :(");
                     Thread.Sleep(500);
                 }
             }
         }

        //所有线程都可以同时访问的方法
        public void DoNotLockMe()
         {
             Console.WriteLine("I am not locked :)");
         }
     }
   这次我们使用一个私有成员作为锁定变量(locker),在LockMe中仅仅锁定这个私有locker,而不是整个对象。这时候重新运行程序,可以看到虽然t1出现了死锁,DoNotLockMe()仍然可以由主线程访问;LockMe()依然不能访问,原因是其中锁定的locker还没有被t1释放。
关键点:
    1. lock(this)的缺点就是在一个线程锁定某对象之后导致整个对象无法被其他线程访问。
    2. 锁定的不仅仅是lock段里的代码,锁本身也是线程安全的。
    3. 我们应该使用不影响其他操作的私有对象作为locker。
    4. 在使用lock的时候,被lock的对象(locker)一定要是引用类型的,如果是值类型,将导致每次lock的时候都会将该对象装箱为一个新的引用对象(事实上如果使用值类型,C#编译器(3.5.30729.1)在编译时就会给出一个错误)(上边已说明)。

1.4    Lock为什么不要Lock(null对象)

    以上面实例代码为例,将lock(thisLock)改为Lock(null),会抛出异常

   事实上,lock 关键字就是用Monitor 类来实现的。例如:

lock(x)
{
  DoSomething();
}

这等效于:

System.Object obj = (System.Object)x;
System.Threading.Monitor.Enter(obj);
try
{
  DoSomething();
}
finally
{
  System.Threading.Monitor.Exit(obj);
}

    使用 lock 关键字通常比直接使用 Monitor 类更可取,一方面是因为 lock 更简洁,另一方面是因为 lock 确保了即使受保护的代码引发异常,也可以释放监视器。这是通过 finally 关键字来实现的,无论是否引发异常它都执行关联的代码块。

 这里微软已经说得很清楚了,Lock就是用Monitor实现的,两者都是C#中对临界区功能的实现。用ILDASM打开含有以下代码的exe 或者dll也可以证实这一点(我并没有自己证实):

lock (lockobject)
{
  int i = 5;
}

反编译后的的IL代码为:

IL_0045:  call       void [mscorlib]System.Threading.Monitor::Enter(object
IL_004a:  nop 
.try 

  IL_004b:  nop 
  IL_004c:  ldc.i4.5 
  IL_004d:  stloc.1 
  IL_004e:  nop 
  IL_004f:  leave.s    IL_0059 
}  // end .try 
finally 

  IL_0051:  ldloc.3 
  IL_0052:  call       void [mscorlib]System.Threading.Monitor::Exit(object
  IL_0057:  nop 
  IL_0058:  endfinally 
}  // end handler

 

   而对于Monitor,发现它的静态方法Enter(object obj)有一个异常类型ArgumentNullException,执行lock(null对象 )处,抛出未处理的异常:System.ArgumentNullException: 值不能为空!

1.5    Lock 对象为什么推荐为只读静态对象

    在代码段中修改锁定对象,会出现 blance<0的情况,并会抛出异常

private static readonly object obj = new object();

    为什么要设置成只读的呢?这是因为如果在lock代码段中改变obj的值,其它线程就畅通无阻了,因为互斥锁的对象变了,object.ReferenceEquals必然返回false

 
原文地址:https://www.cnblogs.com/scottckt/p/2236124.html