CLR系列有些时间没更新了,主要时由于最近一个项目太忙。所以没能更新。以后会继续。其他话就不说了,我们来看看一段代码:
2 {
3 public void DoMultiThreadedWork( object someParameter)
4 {
5 lock (lockObject)
6 {
7 // lots of work
8 }
9 }
10
11 private string lockObject = " lockit " ;
12 }
这段代码很简单,大家看看这段代码有什么问题呢?
开始这么一看似乎没什么大的问题。可是仔细分析一下代码你就可以知道其中还是有一些很严重的问题的。假如你对Lock机制有所了解并且对string类型有过研究的话你就会发现出问题:
lock 关键字的参数必须为基于引用类型的对象,该对象用来定义锁的范围。在上例中,锁的范围限定为此函数,因为函数外不存在任何对该对象的引用。严格地说,提供给 lock 的对象只是用来唯一地标识由多个线程共享的资源,所以它可以是任意类实例。然而,实际上,此对象通常表示需要进行线程同步的资源。例如,如果一个容器对象将被多个线程使用,则可以将该容器传递给 lock,而 lock 后面的同步代码块将访问该容器。只要其他线程在访问该容器前先锁定该容器,则对该对象的访问将是安全同步的。通常,最好避免锁定 public 类型或锁定不受应用程序控制的对象实例。因为不受控制的代码也可能会锁定该对象。这可能导致死锁,即两个或更多个线程等待释放同一对象。关于Lock被编译器编译过后的代码请参考我的另一篇文章:Inside C#
String在CLR中有两个重要的属性:不变性和字符串驻留。这意味着整个程序中任何给定字符串都只有一个实例,就是这同一个对象表示了所有运行的应用程序域的所有线程中的该文本。因此,只要在应用程序进程中的任何位置处具有相同内容的字符串上放置了锁,就将锁定应用程序中该字符串的所有实例。因此,最好锁定不会被暂留的私有或受保护成员。
所以锁定字符串尤其危险。
下面我们来看看字符串驻留是个怎样的东东。看看下面的代码
2 string b = " lockit " ;
3 string c = " LOCKIT " .ToLower();
4 string d = " lock " + " it " ;
5 string e1 = " lock " ;
6 string e2 = " it " ;
7 string ee = e1 + " it " ;
8 string e = e1 + e2;
9 Console.WriteLine( object .ReferenceEquals(a, b));
10 Console.WriteLine( object .ReferenceEquals(a, c));
11 Console.WriteLine( object .ReferenceEquals(a, d));
12 Console.WriteLine( object .ReferenceEquals(a, ee));
13 Console.WriteLine( object .ReferenceEquals(a, e));
下边是输出的结果:
True,False,True,False,False
我们知道CLR处理每个对象在内存的时候都会额外的生成一个syncblockindex空间,这个就是用于对象同步用的。是大家平时开发使用最多的一个类型,MS的CLR部门为了简化操作和性能的优化做了两点处理,一是将string的创建过程简单化。一般的对象在创建的时候通过new关键字来实现;而string不需要这么做,我们只需要把对应的字符换赋给给对应的字符串变量就可以了。那么他们在创建过程中使用的MSIL指令时不同的——一般的引用对象的创建是通过newobj这样一个IL指令来实现的,而创建一个字符串变量的IL指令则是ldstr(load string)。其次就是为了考虑性能的提升和内存节约上,对于创建相同的字符串,一般不会为他们分别分配内存块,相反地,他们会共享一块内存。CLR内部维护一个HashTable,这HashTable维护者大部分创建的string。这个HashTable的Key对应的相应的string本身,而Value则是分配给这个string的内存块的引用。我们知道在一个托管进程被创建以后,在托管进程的内存空间里面,包含了System Domain,Shared Domain等等,而这个HashTable就是放在System Domain里,所以它是在这整个程序的生命周期里都是存在的和被共享的。一般地,在程序运行过程中,如果需要的创建一个string,CLR会根据这个string的Hash Code试着在HashTable中找这个相同的string,如果找到,则直接把找到的string的地址赋给相应的变量,如果没有则在托管堆中创建一个string,CLR会先在managed heap中创建该strng,并在HashTable中创建一个Key-Value,Key为这个string本身,Value为string的内存地址,这个地址最重被赋给响应的变量。
上面的例子后面两个为False告诉我们对于对一个动态创建的字符串,驻留机制便不会起作用。这种情况产生的MSIL也不一样。但是我们可以用System.String中的静态方法Intern来解决。
通过以上的分析,回到第一个问题,这段代码会出现什么问题我们就知道了。这可以说明平时工作过程中大家尽量小心自己的代码,可以从一些小的代码看到.NET内部的实现机制以及平时自己写代码过程中的疏忽。
参考文章:http://msdn.microsoft.com/zh-cn/library/ms173179(VS.80).aspx