首先我们来介绍什么是死锁,死锁的出现至少涉及到两个线程,两个线程都在等待对方释放锁,这样两个线程就会无限期的等待,程序无法正常的执行下去。我们可以看下面代码所示的一个简单例子。
class SampleThread { private object s1 = new Object(); private object s2 = new Object(); public void Deadlock1() { for(int i=0;i<100;i++) { lock (s1) { lock (s2) { Console.WriteLine($"Method1 output:{i}"); } } } } public void Deadlock2() { for (int i = 0; i < 100; i++) { lock (s2) { lock (s1) { Console.WriteLine($"Method2 output:{i}"); } } } } }
如果两个线程Thread1,Thread2同时执行Deadlock1和Deadlock2方法,就会有可能出现死锁,因为可能会出现这种情况,Thread1锁定了s1,等待s2锁定的解除,而此时Thread2锁定了s2,等待s1锁定的解除。这两个线程都在等待对方释放锁定,并且将无限等待下去,这是一个典型的死锁。
那么我们在编码的过程中如何避免死锁的产生呢?下面我主要介绍三个方法。
· 使用不同的锁时,按相同的顺序加解锁
· 尝试占用锁时设置超时时间
· 当使用TAP模式的异步编程时,避免混用await关键字和Wait()方法
下面我将详细的介绍这三种方法:
1. 使用不同的锁时,按相同的顺序加解锁
如上文中产生死锁的例子,就是第一个代码块先加A锁再加B锁,第二个代码块先加B锁再加A锁。如果把第二个代码也改成先加A锁再加B锁,那么就不会出现死锁了,因为如果有线程进入到第一个代码块时,会占用A锁,尝试进入第二个代码块的线程会等待A锁的释放。
2. 尝试占用锁时设置超时时间
在给代码块加锁时,可以调用含有设置超时时长参数的api,这样的话,如果长时间获取不到锁,超过了等待时长,程序就会放弃去占有锁的尝试,避免了无限期等待发生死锁。如使用Monitor类时,调用TryEnter方法;使用Mutex类时使用WaitOne(int millisecondsTimeout)带有超时时长参数的的重载方法。
3. 当使用TAP模式的异步编程时,避免混用await关键字和Wait()方法
首先如果混用await关键字和Wait()方法为什么会产生死锁呢,我们来看下面一个例子。
private async void button1_Click(object sender, EventArgs e) { var task = GetNameAsync(); task.Wait(); } private async Task<string> GetNameAsync() { string name= await Task.Run(() => { return "ggx"; }); return name; }
这是一个winform程序,点击button1以后,死锁就产生了,GUI线程会一直阻塞,程序卡死,我们来看下为什么。GUI线程调用GetNameAsync ()方法,遇到await关键字后,GUI线程返回button1_Click方法继续往下执行task.Wait()方法,执行到这里以后,线程阻塞在这里,等待task执行完毕后才能继续,然而Task分配的线程执行完return “ggx”以后,CLR会请求占用GUI线程去执行后续的“return name”,而此时GUI线程还阻塞在那里等待异步方法GetNameAsync 的Task返回结果,这样两个线程都在等待对方执行完毕,就产生了死锁,程序僵持无法执行下去。
如何避免这种死锁呢,基于Task的异步编程(TAP)时有一个原则,即一旦使用了异步方法,那么在调用异步方法时,避免使用Wait、WaitAll、WaitAny这种会阻塞线程的方法,改为使用await关键字、Task.WhenAll()、Task.WhenAnay()等不会阻塞线程的异步调用方法。将代码改为如下,则可避免死锁产生。
private async void button1_Click(object sender, EventArgs e) { var task = GetNameAsync(); await task; }
需要注意的使这种死锁在GUI和Asp.net上下文中一定会发生,否则不一定会发生,因为只有GUI和ASP.Net上下文在执行完异步方法后,会再次占用主线程中去执行后续操作,而其他的上下文则会从线程池中取出空闲的线程执行后续操作,此时如果恰巧又取到主线程,则会产生死锁,而如果“运气好”没有取到主线程,则不会产生死锁。