c#线程死锁

起因

做实施的同事,最近一直想转开发.在WinForm和Web之间徘徊.所以就有了话题.
WinForm会经常遇到多线程,当然也可以是异步处理.
Web这里主要是(Asp.Net WebForm/Asp.Net MVC或者Asp.Net Core)天生是多线程的,很少去开启多线程,执行任务.

多线程使任务可以并行运行.和正常顺序执行是不一样,在使用的时候有很多注意事项.拿典型的说: 线程死锁. 在其他可以使用多线程语言中,线程死锁都是可能发生.

从单线程说起,代码示例

class Program
{
    static int num = 0;

    static void Main(string[] args)
    {
        TestNum();
        Console.WriteLine($"num={num}");
        Console.ReadKey();
    }

    public static void TestNum()
    {
        for (int i = 0; i < 10000000; i++)
        {
            num += 1;  
        }
    }
}

上面的代码,可以很好的运行.计算得到的num,代码没有进行注释.

多线程示例

class Program
{
    static int num = 0;

    static void Main(string[] args)
    {
        Thread t1 = new Thread(TestNum);
        t1.Start();                         //线程1,开始调用TestNum

        Thread t2 = new Thread(TestNum);
        t2.Start();                         //线程2,开始调用TestNum

        t1.Join();                          //等待线程1执行结束
        t2.Join();                          //等待线程2执行结束

        Console.WriteLine($"num={num}");    //输出两个线程执行完的结果
        Console.ReadKey();
    }

    public static void TestNum()
    {
        for (int i = 0; i < 10000000; i++)
        {
            num += 1;
        }
    }
}

执行结果:

两个线程执行结果,为什么和我们预期不一样呢?

为什么值不是我们预期的值呢?先看看下面两张图

对变量加等于的,要读取变量的值,再在值上加一

由于两个线程可能存在同时读取和写入值,造成预期值不是我们想要的.

上面两张图,可以解释为什么没有得到预期的值.所以就有锁的概念.

怎么使用锁?

锁在现实生活中随处可见,比如说在自行车上锁,如果不打开锁,我们就无法骑行.在程序中,我们要锁住某个变量,让其他线程展示无法访问.只能开了锁,只能开了锁才能继续访问.
class Program
{
    static int num = 0;

    static object obj1 = new object();

    static void Main(string[] args)
    {
        Thread t1 = new Thread(TestNum);
        t1.Start();                         //线程1,开始调用TestNum

        Thread t2 = new Thread(TestNum);
        t2.Start();                         //线程2,开始调用TestNum

        t1.Join();                          //等待线程1执行结束
        t2.Join();                          //等待线程2执行结束

        Console.WriteLine($"num={num}");    //输出两个线程执行完的结果
        Console.ReadKey();
    }

    public static void TestNum()
    {
        //lock 在c#中是关键字, 简单可以理解现实中的锁, 在一个线程中锁住obj1变量,其他线程要等待当前执行结束才能访问,
        lock (obj1)   
        {
            for (int i = 0; i < 10000000; i++)
            {
                num += 1;
            }
        }
    }
}

在将上面的代码编译运行后,终于得到我们预期的值.

得出预期值

lock关键字,只是语法糖,是Monitor.Enter的简写

先看看这段代码.
static object obj1 = new object();

static void Main()
{
    lock(obj1)
    {
        Thread.Sleep(1000);
    }
    Console.WriteLine("ok");
    Console.ReadKey();
}

主要通过ILSpy查看生成IL代码.

可以在IL代码中,看到lock编译之后,就会被Monitor.Enter和Monitor.Exit替换.

如果使用Monitor.Enter,代码应该这样写.

static object obj1 = new object();

static void Main()
{
    try
    {
        Monitor.Enter(obj1);
        Thread.Sleep(1000);
    }
    finally
    {
        Monitor.Exit(obj1);
    }
    Console.WriteLine("ok");
    Console.ReadKey();
}

怎么发生死锁? 简单对代码进行升级

class Program
{
    static int num = 0;

    static object obj1 = new object();
    static object obj2 = new object();

    static void Main(string[] args)
    {
        Thread t1 = new Thread(TestDeadLock);
        t1.Start(true);                          //线程1,开始调用TestNum

        Thread t2 = new Thread(TestDeadLock);
        t2.Start(false);                         //线程2,开始调用TestNum

        t1.Join();                               //等待线程1执行结束
        t2.Join();                               //等待线程2执行结束

        Console.WriteLine($"num={num}");         //输出两个线程执行完的结果
        Console.ReadKey();
    }

    /// <summary>
    /// 测试线程死锁
    /// </summary>
    /// <param name="param"></param>
    public static void TestDeadLock(object param)
    {
        bool flag = (bool)param;
        if (flag)
        {   //线程1执行
            lock (obj1)
            {
                Console.WriteLine($"lock obj1 flag={flag}");
                lock (obj2)
                {
                    Console.WriteLine($"lock obj2 flag={flag}");
                    for (int i = 0; i < 10000000; i++)
                    {
                        num += 1;
                    }
                }
            }
        }
        else
        {
            //线程2执行
            lock (obj2)
            {
                Console.WriteLine($"lock obj2 flag={flag}");
                lock (obj1)
                {
                    Console.WriteLine($"lock obj1 flag={flag}");
                    for (int i = 0; i < 10000000; i++)
                    {
                        num += 1;
                    }
                }
            }
        }
    }       
}
为什么会出现线程死锁

线程死锁注意事项

  1. 尽量不共享全局变量.
  2. 避免线程多次锁.
  3. 在使用多线程时,设计要合理
到这里,线程死锁大体上讲完了,不知道我那同事明白了多少了.将东西给人讲的明白透彻不是一件轻松的事.
秋风 2018-05-14