C# Lock 解读[转]

一、Lock定义

    lock 关键字可以用来确保代码块完成运行,而不会被其他线程中断。它可以把一段代码定义为互斥段(critical section),互斥段在一个时刻内只允许一个线程进入执行,而其他线程必须等待。这是通过在代码块运行期间为给定对象获取互斥锁来实现的。

在多线程中,每个线程都有自己的资源,但是代码区是共享的,即每个线程都可以执行相同的函数。这可能带来的问题就是几个线程同时执行一个函数,导致数据的混乱,产生不可预料的结果,因此我们必须避免这种情况的发生。

而在.NET中最好了解一下进程、应用域和线程的概念,因为Lock是针对线程一级的,而在.NET中应用域是否会对Lock起隔离作用,我的猜想是,即不在同一应用域中的线程无法通过Lock来中断;另外也最好能了解一下数据段、代码段、堆、栈等概念。

在C# lock关键字定义如下:

lock(expression) statement_block,其中expression代表你希望跟踪的对象,通常是对象引用。

如果你想保护一个类的实例,一般地,你可以使用this;如果你想保护一个静态变量(如互斥代码段在一个静态方法内部),一般使用类名就可以了。

而statement_block就是互斥段的代码,这段代码在一个时刻内只可能被一个线程执行。

二、简单例子

using System; 
using System.Collections; 
using System.Collections.Generic; 
using System.Threading; 
namespace ConsoleApplication1 

    class Program 
    { 
        static void Main(string[] args) 
        { 
            Thread thread1 = new Thread(new ThreadStart(ThreadStart1)); 
            thread1.Name = "Thread1"; 
            Thread thread2 = new Thread(new ThreadStart(ThreadStart2)); 
            thread2.Name = "Thread2"; 
            Thread thread3 = new Thread(new ThreadStart(ThreadStart3)); 
            thread3.Name = "Thread3"; 
            thread1.Start(); 
            thread2.Start(); 
            thread3.Start(); 
           Console.ReadKey(); 
      } 
      static object _object = new object(); 
      static void Done(int millisecondsTimeout) 
      { 
            Console.WriteLine(string.Format("{0} -> {1}.Start", DateTime.Now.ToString("HH:mm:ss"), Thread.CurrentThread.Name)); 
            //下边代码段同一时间只能由一个线程在执行 
            lock (_object) 
            { 
                  Console.WriteLine(string.Format("{0} -> {1}进入锁定区域.", DateTime.Now.ToString("HH:mm:ss"), Thread.CurrentThread.Name)); 
                  Thread.Sleep(millisecondsTimeout); 
                 Console.WriteLine(string.Format("{0} -> {1}退出锁定区域.", DateTime.Now.ToString("HH:mm:ss"), Thread.CurrentThread.Name)); 
            } 
      } 
      static void ThreadStart1() 
      { 
             Done(5000); 
      } 
      static void ThreadStart2() 
      { 
             Done(3000); 
      } 
      static void ThreadStart2() 
      { 
             Done(1000); 
      } 
   } 
}

三、简单解释一下执行过程

先来看看执行过程,代码示例如下:

private static object  ojb = new object();

lock(obj)

{

//锁定运行的代码段


  假设线程A先执行,线程B稍微慢一点。线程A执行到lock语句,判断obj是否已申请了互斥锁,判断依据是逐个与已存在的锁进行object.ReferenceEquals比较(此处未加证实),如果不存在,则申请一个新的互斥锁,这时线程A进入lock里面了。

这时假设线程B启动了,而线程A还未执行完lock里面的代码。线程B执行到lock语句,检查到obj已经申请了互斥锁,于是等待;直到线程A执行完毕,释放互斥锁,线程B才能申请新的互斥锁并执行lock里面的代码。

四、Lock的对象选择问题

接下来说一些lock应该锁定什么对象。

    1、为什么不能lock值类型

比如lock(1)呢?lock本质上Monitor.Enter,Monitor.Enter会使值类型装箱,每次lock的是装箱后的对象。lock其实是类似编译器的语法糖,因此编译器直接限制住不能lock值类型。退一万步说,就算能编译器允许你lock(1),但是object.ReferenceEquals(1,1)始终返回false(因为每次装箱后都是不同对象),也就是说每次都会判断成未申请互斥锁,这样在同一时间,别的线程照样能够访问里面的代码,达不到同步的效果。同理lock((object)1)也不行。

    2、Lock字符串

那么lock("xxx")字符串呢?MSDN上的原话是:

锁定字符串尤其危险,因为字符串被公共语言运行库 (CLR)“暂留”。 这意味着整个程序中任何给定字符串都只有一个实例,就是这同一个对象表示了所有运行的应用程序域的所有线程中的该文本。因此,只要在应用程序进程中的任何位置处具有相同内容的字符串上放置了锁,就将锁定应用程序中该字符串的所有实例。

    3、MSDN推荐的Lock对象

通常,最好避免锁定 public 类型或锁定不受应用程序控制的对象实例。例如,如果该实例可以被公开访问,则 lock(this) 可能会有问题,因为不受控制的代码也可能会锁定该对象。这可能导致死锁,即两个或更多个线程等待释放同一对象。出于同样的原因,锁定公共数据类型(相比于对象)也可能导致问题。

而且lock(this)只对当前对象有效,如果多个对象之间就达不到同步的效果。

而自定义类推荐用私有的只读静态对象,比如:

private static readonly object obj = new object();

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

4、lock(typeof(Class))

与锁定字符串一样,范围太广了。

五、特殊问题:Lock(this)等的详细解释

在以前编程中遇到lock问题总是使用lock(this)一锁了之,出问题后翻看MSDN突然发现下面几行字:通常,应避免锁定 public 类型,否则实例将超出代码的控制范围。常见的结构 lock (this)、lock (typeof (MyType)) 和 lock ("myLock") 违反此准则:如果实例可以被公共访问,将出现C# lock this问题。如果 MyType 可以被公共访问,将出现 lock (typeof (MyType)) 问题。由于进程中使用同一字符串的任何其他代码将共享同一个锁,所以出现 lock(“myLock”) 问题。

来看看C# lock this问题:如果有一个类Class1,该类有一个方法用lock(this)来实现互斥:

  1. publicvoidMethod2()
  2. {
  3. lock(this)
  4. {
  5. System.Windows.Forms.MessageBox.Show("Method2End");
  6. }
  7. }

如果在同一个Class1的实例中,该Method2能够互斥的执行。但是如果是2个Class1的实例分别来执行Method2,是没有互斥效果的。因为这里的lock,只是对当前的实例对象进行了加锁。

Lock(typeof(MyType))锁定住的对象范围更为广泛,由于一个类的所有实例都只有一个类型对象(该对象是typeof的返回结果),锁定它,就锁定了该对象的所有实例,微软现在建议,不要使用lock(typeof(MyType)),因为锁定类型对象是个很缓慢的过程,并且类中的其他线程、甚至在同一个应用程序域中运行的其他程序都可以访问该类型对象,因此,它们就有可能代替您锁定类型对象,完全阻止您的执行,从而导致你自己的代码的挂起。

锁住一个字符串更为神奇,只要字符串内容相同,就能引起程序挂起。原因是在.NET中,字符串会被暂时存放,如果两个变量的字符串内容相同的话,.NET会把暂存的字符串对象分配给该变量。所以如果有两个地方都在使用lock(“my lock”)的话,它们实际锁住的是同一个对象。到此,微软给出了个lock的建议用法:锁定一个私有的static 成员变量。

.NET在一些集合类中(比如ArrayList,HashTable,Queue,Stack)已经提供了一个供lock使用的对象SyncRoot,用Reflector工具查看了SyncRoot属性的代码,在Array中,该属性只有一句话:return this,这样和lock array的当前实例是一样的。ArrayList中的SyncRoot有所不同

  1. get
  2. {
  3. if(this._syncRoot==null)
  4. {
  5. Interlocked.CompareExchange(refthis._syncRoot,newobject(),null);
  6. }
  7. returnthis._syncRoot;

其中Interlocked类是专门为多个线程共享的变量提供原子操作(如果你想锁定的对象是基本数据类型,那么请使用这个类),CompareExchange方法将当前syncRoot和null做比较,如果相等,就替换成new object(),这样做是为了保证多个线程在使用syncRoot时是线程安全的。集合类中还有一个方法是和同步相关的:Synchronized,该方法返回一个对应的集合类的wrapper类,该类是线程安全的,因为他的大部分方法都用lock来进行了同步处理,比如Add方法:

  1. publicoverridevoidAdd(objectkey,objectvalue)
  2. {
  3. lock(this._table.SyncRoot)
  4. {
  5. this._table.Add(key,value);
  6. }
  7. }

这里要特别注意的是MSDN提到:从头到尾对一个集合进行枚举本质上并不是一个线程安全的过程。即使一个集合已进行同步,其他线程仍可以修改该集合,这将导致枚举数引发异常。若要在枚举过程中保证线程安全,可以在整个枚举过程中锁定集合:

  1. QueuemyCollection=newQueue();
  2. lock(myCollection.SyncRoot){
  3. foreach(ObjectiteminmyCollection){
  4. //Insertyourcodehere.
  5. }
  6. }

最后

注意:应避免锁定 public 类型,否则实例将超出代码的控制范围。常见的结构 lock (this)、lock (typeof (MyType)) 和 lock ("myLock") 违反此准则: 
    1)如果实例可以被公共访问,将出现 lock (this) 问题; 
    2)如果 MyType 可以被公共访问,将出现 lock (typeof (MyType)) 问题; 
    3)由于进程中使用同一字符串的任何其他代码将共享同一个锁,所以出现 lock("myLock") 问题; 
    最佳做法是定义 private 对象来锁定, 或 private static 对象变量来保护所有实例所共有的数据。

六、参考资料

由于参考的资料都保存在本地,只能先列出标题,无法提供原文地址,深表歉意!

1)描述C#多线程中Lock关键字

2)解决C# lock this问题

3)基于C#中的lock关键字的总结

4)C# lock关键字

original:http://www.cnblogs.com/zhanglb/archive/2012/01/02/2310420.html

时间: 2024-10-10 07:51:00

C# Lock 解读[转]的相关文章

C# Lock 解读 (关键是理解最后一句)

最近在研究.NET分布式缓存代码,正好涉及Lock,看了网上的文章,总结了一些Lock相关的知识,供大家一起学习参考. 一.Lock定义     lock 关键字可以用来确保代码块完成运行,而不会被其他线程中断.它可以把一段代码定义为互斥段(critical section),互斥段在一个时刻内只允许一个线程进入执行,而其他线程必须等待.这是通过在代码块运行期间为给定对象获取互斥锁来实现的.      在多线程中,每个线程都有自己的资源,但是代码区是共享的,即每个线程都可以执行相同的函数.这可能

C# Lock 解读

一.Lock定义     lock 关键字可以用来确保代码块完成运行,而不会被其他线程中断.它可以把一段代码定义为互斥段(critical section),互斥段在一个时刻内只允许一个线程进入执行,而其他线程必须等待.这是通过在代码块运行期间为给定对象获取互斥锁来实现的. 在多线程中,每个线程都有自己的资源,但是代码区是共享的,即每个线程都可以执行相同的函数.这可能带来的问题就是几个线程同时执行一个函数,导致数据的混乱,产生不可预料的结果,因此我们必须避免这种情况的发生. 而在.NET中最好了

mysql之show engine innodb status解读(转)

add by zhj: 我第一次知道这个命令是线上服务出了问题,然后同事用这个命令去查看死锁.但用这个命令看死锁有一定的局限性,它只能看到最后一次死锁, 而且只能看到死锁环中的两个事务所执行的最后一条语句(即被死锁卡住的那条语句),看不到整个死锁环,也看到不整个事务的语句.但是即使这亲,对我 们来说也非常有用,因为一般来说,数据库同时存在多个死锁环的可能性比较小,而且有了死锁环中的事务的最后一条语句,我们找到整个死锁环不是太难. "show engine innodb status"这

深入理解JAVA集合系列二:ConcurrentHashMap源码解读

HashMap和Hashtable的区别 在正式开始这篇文章的主题之前,我们先来比较下HashMap和Hashtable之间的差异点: 1.Hashtable是线程安全的,它对外提供的所有方法都是都使用了synchronized,是同步的,而HashMap是非线程安全的. 2.Hashtable不允许value为空,否则会抛出空指针异常: 而HashMap中key.value都可以为空. 1 public synchronized V put(K key, V value) { 2 // Mak

JVM源码分析之堆外内存完全解读

概述 广义的堆外内存 说到堆外内存,那大家肯定想到堆内内存,这也是我们大家接触最多的,我们在jvm参数里通常设置-Xmx来指定我们的堆的最大值,不过这还不是我们理解的Java堆,-Xmx的值是新生代和老生代的和的最大值,我们在jvm参数里通常还会加一个参数-XX:MaxPermSize来指定持久代的最大值,那么我们认识的Java堆的最大值其实是-Xmx和-XX:MaxPermSize的总和,在分代算法下,新生代,老生代和持久代是连续的虚拟地址,因为它们是一起分配的,那么剩下的都可以认为是堆外内存

mysql之show engine innodb status解读

注:以下内容为根据<高性能mysql第三版>和<mysql技术内幕innodb存储引擎>的innodb status部分的个人理解,如果有错误,还望指正!! innodb存储引擎在show engine innodb status(老版本对应的是show innodb status)输出中,显示除了大量的内部信息,它输出就是一个单独的字符串,没有行和列,内容分为很多小段,每一段对应innodb存储引擎不同部分的信息,其中有一些信息对于innodb开发者来说非常有用,但是,许多信息,

hadoop源码解读namenode高可靠:HA;web方式查看namenode下信息;dfs/data决定datanode存储位置

点击browserFilesystem,和命令查看结果一样 当我们查看hadoop源码时,我们看到hdfs下的hdfs-default.xml文件信息 我们查找${hadoop.tmp.dir}这是引用变量,肯定在其他文件有定义,在core-default.xml中查看到,这两个配置文件有个共同点: 就是不要修改此文件,但可以复制信息到core-site.xml和hdfs-site.xml中修改 usr/local/hadoop 是我存放hadoop文件夹的地方 几个关于namenode的重要文

Python进阶(三十四)-Python3多线程解读

Python进阶(三十四)-Python3多线程解读 线程讲解 ??多线程类似于同时执行多个不同程序,多线程运行有如下优点: 使用线程可以把占据长时间的程序中的任务放到后台去处理. 用户界面可以更加吸引人,这样比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度. 程序的运行速度可能加快. 在一些等待的任务实现上如用户输入.文件读写和网络收发数据等,线程就比较有用了.在这种情况下我们可以释放一些珍贵的资源如内存占用等等. ??线程在执行过程中与进程还是有区别的.每个独立

jQuery源码解读第5章---对Callbacks的解读

jQuery.Callbacks() 是一个多用途的回调函数列表对象 提供了一种强大的方法来管理回调函数队列 先来看看Callbacks的常见的用法 1-------不带参数 先看看不用回调函数的例子 eq function a1(){ console.log('a1') } (function(){ function a2(){ console.log('a2') } })() a1() // a1 a2() //就不行了 这时候我们就可以使用回调函数Callbacks 了 var dfd1