发布博文
随想
读书
音乐
其他
我想爱,想吃,还想在一瞬间变成天上半明半暗的云。
我想爱,想吃,还想在一瞬间变成天上半明半暗的云。
我想爱,想吃,还想在一瞬间变成天上半明半暗的云。
我想爱,想吃,还想在一瞬间变成天上半明半暗的云。

C#中的死锁及避免死锁的方法

649
高光翔
2019-07-02 20:31

      死锁.jpg

      首先我们来介绍什么是死锁,死锁的出现至少涉及到两个线程,两个线程都在等待对方释放锁,这样两个线程就会无限期的等待,程序无法正常的执行下去。我们可以看下面代码所示的一个简单例子。

    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上下文在执行完异步方法后,会再次占用主线程中去执行后续操作,而其他的上下文则会从线程池中取出空闲的线程执行后续操作,此时如果恰巧又取到主线程,则会产生死锁,而如果“运气好”没有取到主线程,则不会产生死锁。

Insert title here Insert title here
打  赏