C#Lock⽤法
最近在研究.NET分布式缓存代码,正好涉及Lock,看了⽹上的⽂章,总结了⼀些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;
typeof的用法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有所不同
8. get
9. {
10. if(this._syncRoot==null)
11. {
12. Interlocked.CompareExchange(refthis._syncRoot,newobject(),null);
13. }
14. returnthis._syncRoot;
其中Interlocked类是专门为多个线程共享的变量提供原⼦操作(如果你想锁定的对象是基本数据类型,那么请使⽤这个
类),CompareExchange⽅法将当前syncRoot和null做⽐较,如果相等,就替换成new object(),这样做是为了保证多个线程在使⽤syncRoot时是线程安全的。集合类中还有⼀个⽅法是和同步相关的:Synchronized,该⽅法返回⼀个对应的集合类的wrapper 类,该类是线程安全的,因为他的⼤部分⽅法都⽤lock来进⾏了同步处理,⽐如Add⽅法:
15. publicoverridevoidAdd(objectkey,objectvalue)
16. {
17. lock(this._table.SyncRoot)
18. {
19. this._table.Add(key,value);
20. }
21. }
这⾥要特别注意的是MSDN提到:从头到尾对⼀个集合进⾏枚举本质上并不是⼀个线程安全的过程。即使⼀个集合已进⾏同步,其他线程仍可以修改该集合,这将导致枚举数引发异常。若要在枚举过程中保证线程安全,可以在整个枚举过程中锁定集合:
22. QueuemyCollection=newQueue();
23. lock(myCollection.SyncRoot){
24. foreach(ObjectiteminmyCollection){
25. //Insertyourcodehere.
26. }
27. }
最后
注意:应避免锁定 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关键字

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。