zoukankan      html  css  js  c++  java
  • 基元线程同步构造之用户模式易变构造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会导致这些优化消失,影响性能.

  • 相关阅读:
    八.正文处理命令及tar命令
    七.用户.群组及权限的深入讨论
    六.用户.群组和权限
    五.目录,文件的浏览,管理和维护
    四.linux 命令及获取帮助
    计算机的基础知识
    三.linux基本的50条命令
    二.Python的基本数据类型及常用功能
    一.编码的转换和基本的算法
    Linux开机自动挂载Windows分区
  • 原文地址:https://www.cnblogs.com/GreenLeaves/p/15525049.html
Copyright © 2011-2022 走看看