c# 高级编程 21章477页 【任务和并行编程】【条件争用】【死锁】

89 阅读4分钟

线程问题

在启动访问相同数据的多个线程时,会间歇性地遇到难以发现的问题。

使用Task, 并行LINQ, Parallel类,也会遇到这些问题

为避免这些问题,必须特别注意同步等问题

争用条件

如果两个或多个线程访问相同的对象,并且对共享状态的访问没有同步,就会出现争用条件

        static void Main(string[] args)
        {
            if (args.Length != 1)
            {
                ShowUsage();
                return;
            }

            switch (args[0].ToLower())
            {
                case "-r":
                    RaceConditions();
                    break;
                default:
                    ShowUsage();
                    break;
            }
            //等待用户输入
            Console.ReadLine();
        }

主线程等待用户输入。但是因为可能出现条件争用,所以程序很有可能在读取用户输入前就挂起

        public static void RaceConditions()
        {
            var state = new StateObject();
            for (int i = 0; i < 2; i++)
            {
                Task.Run(() => new SampleTask().RaceCondition(state));
            }
        }
    public class StateObject
    {
        private int _state = 5;
        private object _sync = new object();

        public void ChangeState(int loop)
        {
            if (_state == 5)
            {
                _state++;
                if (_state != 6)
                {
                    Console.WriteLine($"Race condition occured after {loop} loops");
                    Trace.Fail($"race condition at {loop}");
                }
             }
             _state = 5;
        }
    }
        public void RaceCondition(object o)
        {
            Trace.Assert(o is StateObject, "o must be of type StateObject");
            StateObject state = o as StateObject;
            Console.WriteLine("starting RaceCondition - when does the issue occur?");

            int i = 0;
            while (true)
            {
                state.ChangeState(i++);
            }
        }

输出:

多久之后 出现条件争用,取决于:

  • 系统的CPU核数
    • 多核CPU多个线程可以同时运行,问题出现的次数会比较多
    • 单核CPU线程调度是抢占式的也会出现问题,只是没有那么频繁
  • 编译为 Debug版还是Release版
    • Release版:问题出现的次数比较多,因为代码被优化
>dotnet ThreadingIssues.dll -r
starting RaceCondition - when does the issue occur?
starting RaceCondition - when does the issue occur?
Race condition occured after 701 loops
Race condition occured after 406367 loops
Race condition occured after 123313 loops
Race condition occured after 153449 loops
Race condition occured after 490266 loops
Race condition occured after 507956 loops
Race condition occured after 530763 loops
Race condition occured after 538209 loops
Race condition occured after 550434 loops
Race condition occured after 584099 loops
Race condition occured after 590635 loops
Race condition occured after 300541 loops
Race condition occured after 602228 loops
Race condition occured after 635912 loops
Race condition occured after 352974 loops
Race condition occured after 670992 loops
Race condition occured after 411237 loops
Race condition occured after 762662 loops
Race condition occured after 767264 loops
...

解决办法一:

锁定 共享的对象 这里, 多个线程 共享的对象是 StateObject 类型的实例 state

        public void RaceCondition(object o)
        {
            Trace.Assert(o is StateObject, "o must be of type StateObject");
            StateObject state = o as StateObject;
            Console.WriteLine("starting RaceCondition - when does the issue occur?");

            int i = 0;
            while (true)
            {
                lock (state) // no race condition with this lock
                {
                    state.ChangeState(i++);
                }
            }
        }

输出:

>dotnet ThreadingIssues.dll -r
starting RaceCondition - when does the issue occur?
starting RaceCondition - when does the issue occur?

解决办法二:

共享对象类型 设计为 线程安全的 这里,将 StateObject 设计为 线程安全的 只有 引用类型 才能用于锁定 由于不能锁定int类型的_state本身,因此定义一个object类型的变量_sync,将它用于lock语句

    public class StateObject
    {
        private int _state = 5;
        private object _sync = new object();

        public void ChangeState(int loop)
        {
            lock (_sync)
            {
                if (_state == 5)
                {
                    _state++;
                    if (_state != 6)
                    {
                        Console.WriteLine($"Race condition occured after {loop} loops");
                        Trace.Fail($"race condition at {loop}");
                    }
                }
                _state = 5;
            }
        }
    }

输出:

>dotnet ThreadingIssues.dll -r
starting RaceCondition - when does the issue occur?
starting RaceCondition - when does the issue occur?

死锁

过多的锁定也会有麻烦

在死锁中,至少有两个线程被挂起,并等待对方解除锁定。

由于两个线程都在等待对方,就出现了死锁,线程将无限等待下去

        static void Main(string[] args)
        {
            if (args.Length != 1)
            {
                ShowUsage();
                return;
            }

            switch (args[0].ToLower())
            {
                case "-d":
                    Deadlock();
                    break;
                default:
                    ShowUsage();
                    break;
            }

            Console.ReadLine();
        }
        public static void Deadlock()
        {
            var s1 = new StateObject();
            var s2 = new StateObject();
            Task.Run(() => new SampleTask(s1, s2).Deadlock1());
            Task.Run(() => new SampleTask(s1, s2).Deadlock2());

            Task.Delay(10000).Wait();
        }
    public class SampleTask
    {
        //internal static int a;
        //private static Object sync = new object();

        public SampleTask()
        {

        }

        public SampleTask(StateObject s1, StateObject s2)
        {
            _s1 = s1;
            _s2 = s2;
        }

        private StateObject _s1;
        private StateObject _s2;
        
        public void Deadlock1()
        {
            int i = 0;
            while (true)
            {
                Console.WriteLine("1 - waiting for s1");
                lock (_s1)
                {
                    Console.WriteLine("1 - s1 locked, waiting for s2");
                    lock (_s2)
                    {
                        Console.WriteLine("1 - s1 and s2 locked, now go on...");
                        _s1.ChangeState(i);
                        _s2.ChangeState(i++);
                        Console.WriteLine($"1 still running, i: {i}");
                    }
                }
            }
        }

        public void Deadlock2()
        {
            int i = 0;
            while (true)
            {
                Console.WriteLine("2 - waiting for s2");
                lock (_s2)
                {
                    Console.WriteLine("2 - s2 locked, waiting for s1");
                    lock (_s1)
                    {
                        Console.WriteLine("2 - s1 and s2 locked, now go on...");
                        _s1.ChangeState(i);
                        _s2.ChangeState(i++);
                        Console.WriteLine($"2 still running, i: {i}");
                    }
                }
            }
        }
    }
  • Deadlock1()和Deadlock2()在改变两个对象_s1 和 _s2的状态,因此设计了两个锁

输出:

死锁问题发生的频率 取决于系统配置。每次运行结果都不同

>dotnet ThreadingIssues.dll -d
1 - waiting for s1
1 - s1 locked, waiting for s2
1 - s1 and s2 locked, now go on...
2 - waiting for s2
1 still running, i: 1
1 - waiting for s1
1 - s1 locked, waiting for s2
2 - s2 locked, waiting for s1


解决方法: 本例中,只要改变锁定顺序,让这两个线程以相同的顺序进行锁定,就可以了

死锁问题,并不总是这么明显

较大的应用程序中,锁定可能隐藏在方法的深处

为避免死锁,从一开始就设计好 锁定顺序, 也可以为锁定 定义超时时间