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