基元线程同步构造之用户模式-易变构造volatile

此文例子有误,有时间会更正

前文介绍了基元线程同步构造,主要说了线程协调在用户模式和内核模式下的优缺点,本文将在此基础上介绍实际的应用案列.

1、原子性

CLR保证大部分值类型和引用类型的读写是原子性的,如下代码:

        private int param = 0;

        /// <summary>
        /// 线程一任务
        /// </summary>
        public void Task1()
        {
            param = 1;
        }

        /// <summary>
        /// 线程二任务
        /// </summary>
        public void Task2()
        {
            Console.WriteLine(param);
        }

假设有一个变量param,一个线程给param赋值1,另一个线程读取.原子性意味着,在赋值线程操作的时候,读取线程是无法读到0和1之间的值的,只会读到0或者1.写入也是一样,只会写入成功,或者保持原值,不会存在中间的值.这一点CLR提供了保障.输出只能是0或者1;所以可以得出结论变量的读或者写是原子性的.

 

2、多线程变量值写入的无序性

如果只有一个主线程的情况下,我们写的代码会按照我们组织的逻辑运行,可能实际的做法可能和我们写的代码不一样,但是结果往往是意料之中的,但是在多线程环境下,结果可能是出乎意料的.因为编译器会做一些优化,如下代码:

    public class VolatileTests 
    {
        private int flag = 0;
        private int value = 0;

        /// <summary>
        /// 线程一任务
        /// </summary>
        public void Task1()
        {
            flag = 6;
            value = 5;
        }

        /// <summary>
        /// 线程二任务
        /// </summary>
        public  void Task2()
        {
            if (flag == 0 && value == 5)
            {
                Program._switch = true;
                Console.WriteLine(flag);
                Console.WriteLine(value);
            }
        }
    }

调用代码如下:

        public static bool _switch = false;
        static void Main(string[] args)
        {
            while (!_switch)
            {
                var t = new VolatileTests();
                Task.Run(() =>
                {
                    t.Task1();
                });
                Task.Run(() =>
                {
                    t.Task2();
                });
                GC.Collect();
            }
            Console.ReadKey();
        }

,结果出乎意料,分析一下

 当执行线程二任务时,编译器将flag和value从ram读取到cpu寄存器,根据输出推断出,读到的flag是0,那么存在以下几种可能的情况:

(1)、线程二任务,先于线程一任务执行

(2)、线程一任务已经执行,线程二任务的cpu寄存器没有感知到flag已经被线程一线程修改

重点来了,value值为5,说明编译器确实介入了.线程一任务的赋值操作不是按顺序执行的而是随机的.接着根据最后的输出发现flag的变量为6,所以说明赋值操作,在判断操作之后被执行了.

结论:根据代码和输出结果推断,多线程的执行时间和变量写入是无序的,多个线程在操作共享变量时,其顺序也是无序的,且参数赋值操作也是无序的.所以去掉判断,重新运行主程序,输出会产生很多结果.

问题:如何解决这种无序性.通过基元线程同步构造之用户模式-易变构造volatile来解决.

 

3、解决编译器的优化导致多线程访问共享变量产生的数据紊乱问题

那么如何让线程任务二访问到线程任务一设置的正确值呢?

通过System.Threading.Volatile静态类能解决一部分线程协作同步问题,其下面的方法能禁止C#编译器、JIT编译器以及CPU进行的一些优化(随机赋值等优化措施).其下常用方法主要为两种

Write方法:按照原先的编码顺序,所有的加载赋值操作必须在Wite方法之前完成.

Read方法:按照原先的编码顺序,所有的加载赋值操作必须在Read方法之后完成.

总结一句话,就是Write写入最后一个值,Read读取第一个值.

注意:在Write方法之前的和在Read方法之后的加载和赋值操作任然是无序的,代码如下:

public class VolatileTests
    {
        private int flag = 0;
        private int value = 0;

        /// <summary>
        /// 线程一任务
        /// </summary>
        public void Task1()
        {
            flag = 6;
           Volatile.Write(ref value,5);//value变量写入5之前   flag已经写入6
        }

        /// <summary>
        /// 线程二任务
        /// </summary>
        public void Task2()
        {
            //在读取到value为5之后在读取别的变量值
            if (Volatile.Read(ref value) == 5)
            {
                if (flag != 6 || value != 5)
                {
                    Console.WriteLine("Volatile方法失效");
                }
                
            }
        }
    }

调用代码如上,,共享变量的值得到了正确的结果.flag为6,value为5.根据C# 基元线程同步构造中的用户模式介绍,

 

3、volatile关键字

为了简化调用的复杂度,官方提供了volatile关键字来达到一样的效果,代码如下

public class VolatileTests
    {
        private int flag = 0;
        private volatile int value = 0;

        /// <summary>
        /// 线程一任务
        /// </summary>
        public void Task1()
        {
            flag = 6;
            value = 5;
        }

        /// <summary>
        /// 线程二任务
        /// </summary>
        public void Task2()
        {
            if (value == 5)
            {
                if (flag != 6)
                {
                    Console.WriteLine("volatile关键字失效");
                    Program._switch = true;
                    Console.WriteLine(flag);
                }
                else {
                    Console.WriteLine(flag);
                }
            }
        }
    }

效果是一样的,但是编程体验变好了.

 

注:

(1)、以下方式是不行的

 int.TryParse("可能需要转换的int值", out value);

不需要以传递引用的方式传递给volatile变量.编译器会警告

 

 

(2)、有些编译器级别的优化是必要的.使用volatile会导致这些优化消失,影响性能.

posted @ 2021-11-08 17:09  郑小超  阅读(107)  评论(0编辑  收藏  举报