【C# 线程】 volatile 关键字和Volatile类、Thread.VolatileRead|Thread.VolatileWrite 详细 完整
overview
同步基元分为用户模式和内核模式
用户模式:Iterlocked.Exchange(互锁)、SpinLocked(自旋锁)、易变构造(volatile关键字、
volatile
类、Thread.VolatitleRead|Thread.VolatitleWrite
)、MemoryBarrier。
重要内容来源:C# 中的线程处理 - 第 4 部分 - 高级线程处理 (albahari.com)
==========================volatile简介(多语言共性)==========================
易失性:volatile关键字可以用来提醒编译器它后面所定义的变量随时有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。
原子性:Volatile类型的操作 都具有原子特性,所以线程间无法对其占有,它的值永远是最新的。
顺序性:volatile是内存屏障,防止处理器重新对内存操作进行排序的内存屏障。
.net C#中有3个易失性 相关的类:关键字volatile 、静态类Volatile、Thread线程中方法hread.VolatileRead()|Thread.VolatileWrite()
为何寄存器与内存里的值会不同?
理解volatile用到的知识点:
1、内存模型:CPU硬件有它自己的内存模型,不同的编程语言也有它自己的内存模型。
2、原子性:最小的cpu操作 double 和 long在64位的操作系统中是保持原子性,在32位的操作系统中分两次执行,如果数据类型不支持原子性,则会造成多线程的读取访问数据不一致。
- 如果一组变量总是在相同的锁内进行读写,就可以称为原子的(atomically)读写。假定字段
x
与y
总是在对locker
对象的lock
内进行读取与赋值 - 指令原子性是一个相似但不同的概念:如果一条指令可以在 CPU 上不可分割地执行,那么它就是原子的。
- volatile 关键字只对原子性的数据类型有效。
3、多线程:多线程对一个实例对象的A()和B()方法进行访问操作时候,由于线程访问顺序不同,会引起线程实例对像内部字段读取顺序不一样,导致每次运行的结果不一样。处理这个问题就要给内部字段添加volatile 关键字。
4、编辑器 clr cpu优化:编译器和clr为了提高程序的运行效率会对代码经行优化。这样优化会导致代码顺序发生变化,导致多线程的时候执行的效果不一样。
5、CPU和主存储器的通信模型:包括cpu和 缓冲 寄存器 内存直接的互动。
volatile
关键字(C#)
C# 编程语言提供可变字段,限制对内存操作重新排序的方式。 ECMA 规范规定,可变字段应提供获取-释放语义。
volatile作用:
1、直接读取和写入内存数据,不使用寄存器中的数据。易失性:voldatile关键字首先具有“易失性”,就是不会缓存的意思,声明为volatile变量编译器会强制要求读内存,相关语句不会直接使用上一条语句对应的的寄存器内容,而是重新从内存中读取。
2、不具有可见性,不能保证读取的数据最新,因为无法读取其他cpu core上的store buffer的内容。 store buffer的内容只能通过内存屏障刷新得到。
注意 MSDN 文档指出,使用关键字可确保始终在字段中显示最新的值。这是不正确的,因为正如 案例二中我们所看到的,可以对写入后跟读取进行重新排序。导致出乎我们预期的结果0,0
3、具有原子性。变量具有原子性,操作不具有原子性
4、防止编译器过度优化
5、具有释放和获取的语义,以下详细解说。
6、按引用传递参数或捕获的局部变量不支持该关键字,请使用Volatile类的VolatileRead 和 VolatileWrite方法
8、volatile 它是对指令起作用而不是对语句起用
9、一般用于类内部各个方法共享字段的定义 。
获得语义(
Acquire
):限制对内存操作重新排序的方式 执行完。Acquire语义修饰内存读操作(包括内存修改或者读-修改-写操作)倘若一个字段用volatile 修饰符修饰(read-acquire),那么他就能够防止其后面的所有内存读
或
写操作重排到他的前面。
为了方便理解获得语义,读操作
, 就是使用该变量
请看以下例子:
- C#
- class AcquireSemanticsExample {
- int _a;
- volatile int _b;
- int _c;
- void Foo() {
- int a = _a; // Read 1
- int b = _b; // Read 2 (volatile)Read 1 和 Read 3 是不可变的,而 Read 2 是可变的。 Read 2 不能与 Read 3 互换顺序,但可与 Read 1 互换顺序。 图 2 显示了 Foo 正文的有效重新排序。
- int c = _c; // Read 3
- ...
- }
- }
释放语义(
。Release
):限制对内存操作重新排序的方式。Release语义修饰内存写操作(包括内存修改或者读-修改-写操作)倘若一个字段用volatile 修饰符修饰(write-Release),那么他就能够防止其前面的所有内存读或写操作重排到他的后面。
写操作
, 就是给该变量赋值。 写/读是允许的互换的, /表示屏障
- //C#
- class ReleaseSemanticsExample
- {
- int _a;
- volatile int _b;
- int _c;
- void Foo()
- {
- _a = 1; // Write 1
- _b = 1; // Write 2 (volatile) Write 1 和 Write 3 是非可变的,而 Write 2 是可变的。 Write 2 不能与 Write 1 互换顺序,但可与 Write 3 互换顺序。 图 3 显示了 Foo 正文的有效重新排序。
- _c = 1; // Write 3
- ...
- }
- }
使用案例
1、防止编译器使用循环提升技术
- bool complete = false;
- var t = new Thread(() =>
- {
- bool toggle = false;
- while (!complete) toggle = !toggle;
- });
- t.Start();
- Thread.Sleep(1000);
- complete = true;
- t.Join(); // Blocks indefinitely
这个程序永远不会结束,由于complete变量被缓存在了CPU寄存器中。在while循环中加入Thread.MemoryBarrier能够解决这个问题。开发
另一种更高级的方式来解决上面的问题,那就是考虑使用volatile关键字。Volatile关键字告诉编译器在每一次读操做时生成一个fence,来实现保护保护变量的目的。具体说明能够参见msdn的介绍
以上内如来自MSDN
C# volatile
关键字使用范围:
- 引用类型。
- 指针类型(在不安全的上下文中)。 请注意,虽然指针本身可以是可变的,但是它指向的对象不能是可变的。 换句话说,不能声明“指向可变对象的指针”。
- 简单类型,如
sbyte
、byte
、short
、ushort
、int
、uint
、char
、float
和bool
。不支持double、long和ulong,但是Volatile类支持 - 具有以下基本类型之一的
enum
类型:byte
、sbyte
、short
、ushort
、int
或uint
。 - 已知为引用类型的泛型类型参数。
- IntPtr 和 UIntPtr。
- 其他类型(包括
double
和long double 和 long在64位的操作系统中是保持原子性,在32位的操作系统中分两次执行。
)无法标记为volatile
,因为对这些类型的字段的读取和写入不能保证是原子的。 若要保护对这些类型字段的多线程访问,请使用 Interlocked 类成员或使用lock
语句保护访问权限。
volatile
关键字只能应用于 class
或 struct
的字段。 不能将局部变量声明为 volatile
。
以上内容来源:https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/volatile
错误用法
虽然能输出正确结果,但是违反了线程安全3要素中的可见性,所以这代码在如特殊情境中就会出错。
- Counter counter = new Counter();
- Parallel.Invoke(counter.B, counter.B, counter.B, counter.B);
- Console.WriteLine(counter.X);
- class Counter
- {
- private volatile int x = 0;
- public int X { get => x; }
- public void B()
- {
- for (int i = 0; i < 100; i++)
- {
- x += 1;
- }
- }
- }
正确的是将:x+=1;加锁或者 修改成 Interlocked.Increment(ref x);
正确的用法:
- LockFreeStack<int> reeStac = new();
- for (int i = 1; i <=3; i++)
- {
- Thread se = new Thread(test);
- se.Start();
- }
- void test(){
- for (int i = 0; i < 20; i++)
- {
- reeStac.Push(i);
- }
- }
- public class LockFreeStack<T>
- {
- private volatile Node m_head;
- private class Node { public Node Next; public T Value; }
- public void Push(T item)
- {
- var spin = new SpinWait();
- Node node = new Node { Value = item }, head ;
- while (true)
- {
- head = m_head;
- node.Next = head;
- Console.WriteLine("Processor:{0},Thread{1},priority:{2} count:{3} ", Thread.GetCurrentProcessorId(), Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.Priority,item );
- Node dd = Interlocked.CompareExchange(ref m_head, node, head);//如果相等 就把node赋值给m_head,返回值都是原来的m_head。
- if (dd == head) break;//判断是否赋值成功。成功就跳出死循环。
- spin.SpinOnce();
- Console.WriteLine("Processor:{0},Thread{1},priority:{2} spin.SpinOnce()", Thread.GetCurrentProcessorId(), Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.Priority);
- }
- }
- public bool TryPop(out T result)
- {
- result = default(T);
- var spin = new SpinWait();
- Node head;
- while (true)
- {
- head = m_head;
- if (head == null) return false;
- if (Interlocked.CompareExchange(ref m_head, head.Next, head) == head)
- {
- result = head.Value;
- return true;
- }
- spin.SpinOnce(); //这边使用了spinwait 结构
- }
- }
- }
案例二、
这个案例主要考验对 volatile关键字的理解。
该案例会出乎意料的输出0,0 。为了避免该情况必须使用全内存屏障。
该例子来自:volatile的内存屏障的坑
- using System;
- using System.Threading;
- using System.Threading.Tasks;
- namespace MemoryBarriers
- {
- class Program
- {
- static volatile int x, y, a, b;
- static void Main()
- {
- while (true)
- {
- var t1 = Task.Run(Test1);
- var t2 = Task.Run(Test2);
- Task.WaitAll(t1, t2);
- if (a == 0 && b == 0)
- {
- Console.WriteLine("{0}, {1}", a, b);
- }
- x = y = a = b = 0;
- }
- }
- static void Test1()
- {
- x = 1; // Volatile write (release-fence) 转汇编 mov dword ptr [rax+0xc]
- //方案一 Interlocked.MemoryBarrier();
- //方案二 Interlocked.MemoryBarrierProcessWide();
- a = y; // Volatile read (acquire-fence) 转汇编edx, [rax+8]. 和 mov [rax+0x14], edx.两条指令。 cpu将y获取指令重排序移x=1之前执行。导致a=0
- }
- static void Test2()
- {
- y = 1;// Volatile read (acquire-fence)
- //方案一 Interlocked.MemoryBarrier();
- b = x;// Volatile read (acquire-fence)
- }
- }
- }
Volatile静态类(C#)
(1)在多处理器系统上,易失性写入操作可确保写入内存位置的值立即对所有处理器都可见。 易失性读取操作可获取由任何处理器写入内存位置的最新值,因此用来做线程间读写同步。 这些操作可能需要刷新处理器缓存,这可能会影响性能。
类中的静态和方法读/写变量,同时强制执行(技术上是超集)关键字所做的保证。然而,它们的实现相对低效,因为它们实际上会产生完整的围栏。以下是它们对整数类型的完整实现:
- public static void VolatileWrite (ref int address, int value)
- {
- MemoryBarrier(); address = value;
- }
- public static int VolatileRead (ref int address)
- {
- int num = address; MemoryBarrier(); return num;
- }
VolatileRead能执行以下几种原子读取:
方法
public static byte VolatileRead(ref byte address);
public static double VolatileRead(ref double address);
public static float VolatileRead(ref float address);
public static int VolatileRead(ref int address);
public static IntPtr VolatileRead(ref IntPtr address);
public static long VolatileRead(ref long address);//注意:这里保证long类型的原子读取 ,volatile 关键字不支持这个
public static object VolatileRead(ref object address);
public static sbyte VolatileRead(ref sbyte address);
public static short VolatileRead(ref short address);
public static uint VolatileRead(ref uint address);
public static UIntPtr VolatileRead(ref UIntPtr address);
public static ulong VolatileRead(ref ulong address);//注意:这里保证long类型的原子读取,volatile 关键字不支持这个
public static void Write<T> (ref T location, T value) where T : class;
location T 将对象引用写入的字段。
value T 要写入的对象引用。 立即写入一个引用,以使该引用对计算机中的所有处理器都可见。
public static T Read<T> (ref T location) where T : class;
location T 要读取的字段。
T 对读取的 T 的引用。 无论处理器的数目或处理器缓存的状态如何,该引用都是由计算机的任何处理器写入的最新引用。
案例
仅做案例说明Volatile类的使用,该方法在复杂环境肯定会出错。
Thread类中的VolatileRead和VolatileWrite方法和Volatile关键字(C#)
Thread类来看下其中比较经典的VolatileRead和VolatileWrite方法 不知long和ulong类型、也不支持泛型
VolatileWrite: 该方法作用是,当线程在共享区(临界区)传递信息时,通过此方法来原子性的写入最后一个值 VolatileRead: 该方法作用是,当线程在共享区(临界区)传递信息时,通过此方法来原子性的读取第一个值。 |
- /// <summary>
- /// 本例利用VolatileWrite和VolatileRead来实现同步,来实现一个计算
- /// 的例子,每个线程负责运算1000万个数据,共开启10个线程计算至1亿,
- /// 而且每个线程都无法干扰其他线程工作
- /// </summary>
- class Program
- {
- static Int32 count;//计数值,用于线程同步 (注意原子性,所以本例中使用int32)
- static Int32 value;//实际运算值,用于显示计算结果
- static void Main(string[] args)
- {
- //开辟一个线程专门负责读value的值,这样就能看见一个计算的过程
- Thread thread2 = new Thread(new ThreadStart(Read));
- thread2.Start();
- //开辟10个线程来负责计算,每个线程负责1000万条数据
- for (int i = 0; i < 10; i++)
- {
- Thread.Sleep(20);
- Thread thread = new Thread(new ThreadStart(Write));
- thread.Start();
- }
- Console.ReadKey();
- }
- /// <summary>
- /// 实际运算写操作
- /// </summary>
- private static void Write()
- {
- Int32 temp = 0;
- for (int i = 0; i < 10000000; i++)
- {
- temp += 1;
- }
- value += temp;
- //注意VolatileWrite 在每个线程计算完毕时会写入同步计数值为1,告诉程序该线程已经执行完毕
- //所以VolatileWrite方法类似与一个按铃,往往在原子性的最后写入告诉程序我完成了
- Thread.VolatileWrite(ref count, 1);
- }
- /// <summary>
- /// 显示计算后的数据,使用该方法的线程会死循环等待写
- /// 操作的线程发出完毕信号后显示当前计算结果
- /// </summary>
- private static void Read()
- {
- while (true)
- {
- //一旦监听到一个写操作线执行完毕后立刻显示操作结果
- //和VolatileWrite相反,VolatileRead类似一个门禁,只有原子性的最先读取他,才能达到同步效果
- //同时count值保持最新
- if (Thread.VolatileRead(ref count) > 0)
- {
- Console.WriteLine("累计计数:{1}", Thread.CurrentThread.ManagedThreadId, value);
- //将count设置成0,等待另一个线程执行完毕
- count = 0;
- }
- }
- }
- }
【C# 线程】 volatile 关键字和Volatile类、Thread.VolatileRead|Thread.VolatileWrite 详细 完整的更多相关文章
- 多线程的指令重排问题:as-if-serial语义,happens-before语义;volatile关键字,volatile和synchronized的区别
一.指令重排问题 你写的代码有可能,根本没有按照你期望的顺序执行,因为编译器和 CPU 会尝试指令重排来让代码运行更高效,这就是指令重排. 1.1 虚拟机层面 我们都知道CPU执行指令的时候,访问内存 ...
- 【线程】Volatile关键字
Volatile变量具有 synchronized 的可见性特性,但是不具备原子特性.这就是说线程能够自动发现 volatile变量的最新值.Volatile变量可用于提供线程安全,但是只能应用于非常 ...
- Java并发编程:volatile关键字解析
Java并发编程:volatile关键字解析 volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在 ...
- 从根源上解析 Java volatile 关键字的实现
1.解析概览 内存模型的相关概念 并发编程中的三个概念 Java内存模型 深入剖析Volatile关键字 使用volatile关键字的场景 2.内存模型的相关概念 缓存一致性问题.通常称这种被多个线程 ...
- 白话讲述Java中volatile关键字
一.由一段代码引出的问题 首先我们先来看这样一段代码: public class VolatileThread implements Runnable{ private boolean flag = ...
- 谈谈volatile关键字以及常见的误解
转载请保留以下声明 作者:赵宗晟 出处:https://www.cnblogs.com/zhao-zongsheng/p/9092520.html 近期看到C++标准中对volatile关键字的定义, ...
- 并发编程(一)—— volatile关键字和 atomic包
本文将讲解volatile关键字和 atomic包,为什么放到一起讲呢,主要是因为这两个可以解决并发编程中的原子性.可见性.有序性,让我们一起来看看吧. Java内存模型 JMM(java内存模型) ...
- 全面理解Java内存模型(JMM)及volatile关键字(转载)
关联文章: 深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解Java类加载器(ClassLoad ...
- 全面理解Java内存模型(JMM)及volatile关键字
[版权申明]未经博主同意,谢绝转载!(请尊重原创,博主保留追究权) http://blog.csdn.net/javazejian/article/details/72772461 出自[zejian ...
随机推荐
- 一次神奇的Azure speech to text rest api之旅
错误Max retries exceeded with url: requests.exceptions.ConnectionError: HTTPSConnectionPool(host='%20e ...
- cesium加载gltf模型点击以及列表点击定位弹窗
前言 cesium 官网的api文档介绍地址cesium官网api,里面详细的介绍 cesium 各个类的介绍,还有就是在线例子:cesium 官网在线例子,这个也是学习 cesium 的好素材. 之 ...
- StringBuffer类(增删改查及长度可变原理)
1 package cn.itcast.p2.stringbuffer.demo; 2 3 public class StringBufferDemo { 4 5 public static void ...
- 「JOISC 2014 Day1」巴士走读
「JOISC 2014 Day1」巴士走读 将询问离线下来. 从终点出发到起点. 由于在每个点(除了终点)的时间被过来的边固定,因此如果一个点不被新的边更新,是不会发生变化的. 因此可以按照时间顺序, ...
- AT2645 [ARC076D] Exhausted?
解法一 引理:令一个二分图两部分别为 \(X, Y(|X| \le |Y|)\),若其存在完美匹配当且仅当 \(\forall S \subseteq X, f(S) \ge |S|\)(其中 \(f ...
- JavaCV的摄像头实战之六:保存为mp4文件(有声音)
欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...
- 如何高效地写 Form
工作少不了写"增删改查","增删改查"中的"增"和"改"都与 Form 有关,可以说:提升了 Form 的开发效率,就提 ...
- Java线程池实现原理及其在美团业务中的实践(转)
转自美团技术团队:https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html 随着计算机行业的飞速发展,摩尔定律逐 ...
- docker简介及安装(1)
Docker简介 软件开发中最为麻烦的事情可能就是配置环境了.由于用户使用的操作系统具有多样性,即便使用跨平台的开发语言(如Java和Python)都不能保证代码能够在各种平台下都可以正常的运转,而且 ...
- JavaBean基本概念
JavaBean 是特殊的 Java 类,使用 Java 语言书写,并且遵守 JavaBean API 规范. JavaBean 与其它 Java 类相比而言独一无二的特征: 提供一个默认的无参构造函 ...