C#--lock线程锁
写在前面:
在多线程编程中,可能会有许多线程并发的执行一段代码。在某些情况下,我们希望A中的代码块(B)同步的执行,即同一时刻只有一个线程执行代码块B,这就需要用到锁(lock)。lock 关键字可以用来确保代码块完成运行,而不会被其他线程中断。它可以把一段代码定义为互斥段(critical section),互斥段在一个时刻内只允许一个线程进入执行,而其他线程必须等待,以达到安全访问。
举一个例子:现有十个苹果,张三和李四同时吃这些苹果
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace LockTest { class Program { private static int apple = 10;//10个苹果 static void Main(string[] args) { Thread t1 = new Thread(() => EatApple("张三")); Thread t2 = new Thread(() => EatApple("李四")); t1.IsBackground = true; t2.IsBackground = true; t1.Start(); t2.Start(); Console.ReadKey(); } private static void EatApple(string name) { while (true) { apple -= 1; Console.WriteLine(name + "正在吃苹果"); Thread.Sleep(3000); Console.WriteLine(name + "吃完了,还剩" + apple + "个苹果\n"); if (apple <= 0) break; } } } }
结果是这样的混乱:
然后我们把共同访问的代码加上锁之后:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace LockTest { class Program { private static int apple = 10; //10个苹果 private static object locker = new object();//创建锁 static void Main(string[] args) { Thread t1 = new Thread(() => EatApple("张三")); Thread t2 = new Thread(() => EatApple("李四")); t1.IsBackground = true; t2.IsBackground = true; t1.Start(); t2.Start(); Console.ReadKey(); } private static void EatApple(string name) { while (true) { lock (locker)//加锁 { apple -= 1; Console.WriteLine(name + "正在吃苹果"); Thread.Sleep(3000); Console.WriteLine(name + "吃完了,还剩" + apple + "个苹果\n"); if (apple <= 1)//变为1 不然会吃-1个苹果 break; } } } } }
结果如下,加上锁之后呢,两个人就只能一个人一个人的去拿苹果吃。
%注%:
lock(this) 锁定 当前实例对象,如果有多个类实例的话,lock锁定的只是当前类实例,对其它类实例无影响。 lock(typeof(Model))锁定的是model类的所有实例。 lock(obj)锁定的对象是全局的私有化静态变量。外部无法对该变量进行访问。 lock 确保当一个线程位于代码的临界区时,另一个线程不进入临界区。如果其他线程试图进入锁定的代码,则它将一直等待(即被阻止),直到该对象被释放。 所以,lock的结果好不好,还是关键看锁的谁,如果外边能对这个谁进行修改,lock就失去了作用。所以一般情况下,使用私有的、静态的并且是只读的对象。
1、lock的是必须是引用类型的对象,string类型除外。
2、lock推荐的做法是使用静态的、只读的、私有的对象。
3、保证lock的对象在外部无法修改才有意义,如果lock的对象在外部改变了,对其他线程就会畅通无阻,失去了lock的意义。
不能锁定字符串,锁定字符串尤其危险,因为字符串被公共语言运行库 (CLR)“暂留”。 这意味着整个程序中任何给定字符串都只有一个实例,就是这同一个对象表示了所有运行的应用程序域的所有线程中的该文本。因此,只要在应用程序进程中的任何位置处具有相同内容的字符串上放置了锁,就将锁定应用程序中该字符串的所有实例。通常,最好避免锁定 public 类型或锁定不受应用程序控制的对象实例。例如,如果该实例可以被公开访问,则 lock(this) 可能会有问题,因为不受控制的代码也可能会锁定该对象。这可能导致死锁,即两个或更多个线程等待释放同一对象。出于同样的原因,锁定公共数据类型(相比于对象)也可能导致问题。而且lock(this)只对当前对象有效,如果多个对象之间就达不到同步的效果。lock(typeof(Class))与锁定字符串一样,范围太广了。
lock用法:
其写法如下: Object locker = new Object(); lock(locker) { //此处放置同步执行的代码 }
相当于:
private static object locker = new object();//创建锁 Monitor.Enter(locker); //排它锁 { //此处放置同步执行的代码 } Monitor.Exit(locker); //释放指定对象上的排他锁
Monitor的常用属性和方法:
Enter(Object) 在指定对象上获取排他锁。
Exit(Object) 释放指定对象上的排他锁。
Pulse 通知等待队列中的线程锁定对象状态的更改。
PulseAll 通知所有的等待线程对象状态的更改。
TryEnter(Object) 试图获取指定对象的排他锁。
TryEnter(Object, Boolean) 尝试获取指定对象上的排他锁,并自动设置一个值,指示是否得到了该锁。
Wait(Object) 释放对象上的锁并阻止当前线程,直到它重新获取该锁。
常用的方法有两个,Monitor.Enter(object)方法是获取锁,Monitor.Exit(object)方法是释放锁,这就是Monitor最常用的两个方法,在使用过程中为了避免获取锁之后因为异常,致锁无法释放,所以需要在try{} catch(){}之后的finally{}结构体中释放锁(Monitor.Exit())。
TryEnter(Object)和TryEnter() 方法在尝试获取一个对象上的显式锁方面和 Enter() 方法类似。然而,它不像Enter()方法那样会阻塞执行。如果线程成功进入关键区域那么TryEnter()方法会返回true. 和试图获取指定对象的排他锁。
我们可以通过Monitor.TryEnter(monster, 1000),该方法也能够避免死锁的发生,Monitor.TryEnter(Object,Int32)。
设置1S的超时时间,如果在1S之内没有获得同步锁,则返回false,也就是说,在1秒中后,lockObj还未被解锁,TryEntry方法就会返回false,如果在1秒之内,lockObj被解锁,TryEntry返回true。我们可以使用这种方法来避免死锁
Monitor.Wait和Monitor()Pause()
Wait(object)方法:释放对象上的锁并阻止当前线程,直到它重新获取该锁,该线程进入等待队列。
Pulse方法:只有锁的当前所有者可以使用 Pulse 向等待对象发出信号,当前拥有指定对象上的锁的线程调用此方法以便向队列中的下一个线程发出锁的信号。接收到脉冲后,等待线程就被移动到就绪队列中。在调用 Pulse 的线程释放锁后,就绪队列中的下一个线程(不一定是接收到脉冲的线程)将获得该锁。
另外:Wait 和 Pulse 方法必须写在 Monitor.Enter 和Moniter.Exit 之间。