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()

为何寄存器与内存里的值会不同?

中断发生时,CPU会立即把当前所有寄存器的值存入任务的自己的内存区域里。空出所有寄存器,交给中断去做事。中断处理函数返回后,CPU再把需要唤醒的任务的内存区里寄存器部分内容一一载入到寄存器里,并跳转到上次中断的地址,传入PC指针。这样任务就可以继续执行了。这里的关键就是中断结束后恢复到寄存器的值是从任务私有内存区载入的,而不是从原始变量载入的。所以中断期间对变量的修改就无法立即反应到寄存器里。

理解volatile用到的知识点:

1、内存模型:CPU硬件有它自己的内存模型,不同的编程语言也有它自己的内存模型。
2、原子性:最小的cpu操作  double 和 long在64位的操作系统中是保持原子性,在32位的操作系统中分两次执行,如果数据类型不支持原子性,则会造成多线程的读取访问数据不一致。

  • 如果一组变量总是在相同的锁内进行读写,就可以称为原子的(atomically)读写。假定字段xy总是在对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),那么他就能够防止其后面的所有内存读写操作重排到他的前面。

为了方便理解获得语义,读操作, 就是使用该变量  请看以下例子:

  1. C#
  2. class AcquireSemanticsExample {
  3. int _a;
  4. volatile int _b;
  5. int _c;
  6. void Foo() {
  7. int a = _a; // Read 1
  8. int b = _b; // Read 2 (volatile)Read 1 和 Read 3 是不可变的,而 Read 2 是可变的。 Read 2 不能与 Read 3 互换顺序,但可与 Read 1 互换顺序。 图 2 显示了 Foo 正文的有效重新排序。
  9. int c = _c; // Read 3
  10. ...
  11. }
  12. }

释放语义(Release:限制对内存操作重新排序的方式。Release语义修饰内存写操作(包括内存修改或者读-修改-写操作)倘若一个字段用volatile 修饰符修饰(write-Release),那么他就能够防止其前面的所有内存读或写操作重排到他的后面。

写操作, 就是给该变量赋值。 写/读是允许的互换的, /表示屏障

  1. //C#
  2. class ReleaseSemanticsExample
  3. {
  4. int _a;
  5. volatile int _b;
  6. int _c;
  7. void Foo()
  8. {
  9. _a = 1; // Write 1
  10. _b = 1; // Write 2 (volatile) Write 1 和 Write 3 是非可变的,而 Write 2 是可变的。 Write 2 不能与 Write 1 互换顺序,但可与 Write 3 互换顺序。 图 3 显示了 Foo 正文的有效重新排序。
  11. _c = 1; // Write 3
  12. ...
  13. }
  14. }

使用案例

1、防止编译器使用循环提升技术

  1. bool complete = false;
  2. var t = new Thread(() =>
  3. {
  4. bool toggle = false;
  5. while (!complete) toggle = !toggle;
  6. });
  7. t.Start();
  8. Thread.Sleep(1000);
  9. complete = true;
  10. t.Join(); // Blocks indefinitely

这个程序永远不会结束,由于complete变量被缓存在了CPU寄存器中。在while循环中加入Thread.MemoryBarrier能够解决这个问题。开发

另一种更高级的方式来解决上面的问题,那就是考虑使用volatile关键字。Volatile关键字告诉编译器在每一次读操做时生成一个fence,来实现保护保护变量的目的。具体说明能够参见msdn的介绍

以上内如来自MSDN

C# volatile 关键字使用范围:

  • 引用类型。
  • 指针类型(在不安全的上下文中)。 请注意,虽然指针本身可以是可变的,但是它指向的对象不能是可变的。 换句话说,不能声明“指向可变对象的指针”。
  • 简单类型,如 sbytebyteshortushortintuintcharfloatbool。不支持double、long和ulong,但是Volatile类支持
  • 具有以下基本类型之一的 enum 类型:bytesbyteshortushortintuint
  • 已知为引用类型的泛型类型参数。
  • IntPtrUIntPtr
  • 其他类型(包括 doublelong double 和 long在64位的操作系统中是保持原子性,在32位的操作系统中分两次执行。)无法标记为 volatile,因为对这些类型的字段的读取和写入不能保证是原子的。 若要保护对这些类型字段的多线程访问,请使用 Interlocked 类成员或使用 lock 语句保护访问权限。

volatile 关键字只能应用于 classstruct 的字段。 不能将局部变量声明为 volatile

以上内容来源:https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/volatile

 错误用法

虽然能输出正确结果,但是违反了线程安全3要素中的可见性,所以这代码在如特殊情境中就会出错。

  1. Counter counter = new Counter();
  2. Parallel.Invoke(counter.B, counter.B, counter.B, counter.B);
  3. Console.WriteLine(counter.X);
  4. class Counter
  5. {
  6. private volatile int x = 0;
  7.  
  8. public int X { get => x; }
  9.  
  10. public void B()
  11. {
  12. for (int i = 0; i < 100; i++)
  13. {
  14.  
  15. x += 1;
  16.  
  17. }
  18. }
  19. }

正确的是将:x+=1;加锁或者 修改成  Interlocked.Increment(ref x);

正确的用法:

  1. LockFreeStack<int> reeStac = new();
  2.  
  3. for (int i = 1; i <=3; i++)
  4. {
  5. Thread se = new Thread(test);
  6. se.Start();
  7. }
  8.  
  9. void test(){
  10.  
  11. for (int i = 0; i < 20; i++)
  12. {
  13. reeStac.Push(i);
  14.  
  15. }
  16.  
  17. }
  18.  
  19. public class LockFreeStack<T>
  20. {
  21. private volatile Node m_head;
  22.  
  23. private class Node { public Node Next; public T Value; }
  24.  
  25. public void Push(T item)
  26. {
  27. var spin = new SpinWait();
  28. Node node = new Node { Value = item }, head ;
  29. while (true)
  30. {
  31. head = m_head;
  32. node.Next = head;
  33. Console.WriteLine("Processor:{0},Thread{1},priority:{2} count:{3} ", Thread.GetCurrentProcessorId(), Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.Priority,item );
  34. Node dd = Interlocked.CompareExchange(ref m_head, node, head);//如果相等 就把node赋值给m_head,返回值都是原来的m_head。
  35. if (dd == head) break;//判断是否赋值成功。成功就跳出死循环。
  36. spin.SpinOnce();
  37. Console.WriteLine("Processor:{0},Thread{1},priority:{2} spin.SpinOnce()", Thread.GetCurrentProcessorId(), Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.Priority);
  38. }
  39. }
  40.  
  41. public bool TryPop(out T result)
  42. {
  43. result = default(T);
  44. var spin = new SpinWait();
  45.  
  46. Node head;
  47. while (true)
  48. {
  49.  
  50. head = m_head;
  51. if (head == null) return false;
  52. if (Interlocked.CompareExchange(ref m_head, head.Next, head) == head)
  53. {
  54. result = head.Value;
  55. return true;
  56. }
  57. spin.SpinOnce(); //这边使用了spinwait 结构
  58. }
  59. }
  60. }

案例二、

这个案例主要考验对 volatile关键字的理解。

该案例会出乎意料的输出0,0 。为了避免该情况必须使用全内存屏障。

该例子来自:volatile的内存屏障的坑

  1. using System;
  2. using System.Threading;
  3. using System.Threading.Tasks;
  4.  
  5. namespace MemoryBarriers
  6. {
  7. class Program
  8. {
  9. static volatile int x, y, a, b;
  10. static void Main()
  11. {
  12. while (true)
  13. {
  14. var t1 = Task.Run(Test1);
  15. var t2 = Task.Run(Test2);
  16.  
  17. Task.WaitAll(t1, t2);
  18. if (a == 0 && b == 0)
  19. {
  20. Console.WriteLine("{0}, {1}", a, b);
  21. }
  22.  
  23. x = y = a = b = 0;
  24. }
  25. }
  26.  
  27. static void Test1()
  28. {
  29. x = 1; // Volatile write (release-fence) 转汇编 mov dword ptr [rax+0xc]
  30.  
  31. //方案一 Interlocked.MemoryBarrier();
  32. //方案二 Interlocked.MemoryBarrierProcessWide();
  33.  
  34. a = y; // Volatile read (acquire-fence) 转汇编edx, [rax+8]. 和 mov [rax+0x14], edx.两条指令。 cpu将y获取指令重排序移x=1之前执行。导致a=0
  35. }
  36.  
  37. static void Test2()
  38. {
  39.  
  40. y = 1;// Volatile read (acquire-fence)
  41.  
  42. //方案一 Interlocked.MemoryBarrier();
  43. b = x;// Volatile read (acquire-fence)
  44.  
  45. }
  46. }
  47. }

Volatile静态类(C#)

(1)在多处理器系统上,易失性写入操作可确保写入内存位置的值立即对所有处理器都可见。 易失性读取操作可获取由任何处理器写入内存位置的最新值,因此用来做线程间读写同步。  这些操作可能需要刷新处理器缓存,这可能会影响性能。

类中的静态和方法读/写变量,同时强制执行(技术上是超集)关键字所做的保证。然而,它们的实现相对低效,因为它们实际上会产生完整的围栏。以下是它们对整数类型的完整实现:

  1. public static void VolatileWrite (ref int address, int value)
  2. {
  3. MemoryBarrier(); address = value;
  4. }
  5.  
  6. public static int VolatileRead (ref int address)
  7. {
  8. int num = address; MemoryBarrier(); return num;
  9. }

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:

该方法作用是,当线程在共享区(临界区)传递信息时,通过此方法来原子性的读取第一个值

  1. /// <summary>
  2. /// 本例利用VolatileWrite和VolatileRead来实现同步,来实现一个计算
  3. /// 的例子,每个线程负责运算1000万个数据,共开启10个线程计算至1亿,
  4. /// 而且每个线程都无法干扰其他线程工作
  5. /// </summary>
  6. class Program
  7. {
  8. static Int32 count;//计数值,用于线程同步 (注意原子性,所以本例中使用int32)
  9. static Int32 value;//实际运算值,用于显示计算结果
  10. static void Main(string[] args)
  11. {
  12. //开辟一个线程专门负责读value的值,这样就能看见一个计算的过程
  13. Thread thread2 = new Thread(new ThreadStart(Read));
  14. thread2.Start();
  15. //开辟10个线程来负责计算,每个线程负责1000万条数据
  16. for (int i = 0; i < 10; i++)
  17. {
  18. Thread.Sleep(20);
  19. Thread thread = new Thread(new ThreadStart(Write));
  20. thread.Start();
  21. }
  22. Console.ReadKey();
  23. }
  24.  
  25. /// <summary>
  26. /// 实际运算写操作
  27. /// </summary>
  28. private static void Write()
  29. {
  30. Int32 temp = 0;
  31. for (int i = 0; i < 10000000; i++)
  32. {
  33. temp += 1;
  34. }
  35. value += temp;
  36. //注意VolatileWrite 在每个线程计算完毕时会写入同步计数值为1,告诉程序该线程已经执行完毕
  37. //所以VolatileWrite方法类似与一个按铃,往往在原子性的最后写入告诉程序我完成了
  38. Thread.VolatileWrite(ref count, 1);
  39. }
  40.  
  41. /// <summary>
  42. /// 显示计算后的数据,使用该方法的线程会死循环等待写
  43. /// 操作的线程发出完毕信号后显示当前计算结果
  44. /// </summary>
  45. private static void Read()
  46. {
  47. while (true)
  48. {
  49. //一旦监听到一个写操作线执行完毕后立刻显示操作结果
  50. //和VolatileWrite相反,VolatileRead类似一个门禁,只有原子性的最先读取他,才能达到同步效果
  51. //同时count值保持最新
  52. if (Thread.VolatileRead(ref count) > 0)
  53. {
  54. Console.WriteLine("累计计数:{1}", Thread.CurrentThread.ManagedThreadId, value);
  55. //将count设置成0,等待另一个线程执行完毕
  56. count = 0;
  57. }
  58. }
  59. }
  60.  
  61. }

【C# 线程】 volatile 关键字和Volatile类、Thread.VolatileRead|Thread.VolatileWrite 详细 完整的更多相关文章

  1. 多线程的指令重排问题:as-if-serial语义,happens-before语义;volatile关键字,volatile和synchronized的区别

    一.指令重排问题 你写的代码有可能,根本没有按照你期望的顺序执行,因为编译器和 CPU 会尝试指令重排来让代码运行更高效,这就是指令重排. 1.1 虚拟机层面 我们都知道CPU执行指令的时候,访问内存 ...

  2. 【线程】Volatile关键字

    Volatile变量具有 synchronized 的可见性特性,但是不具备原子特性.这就是说线程能够自动发现 volatile变量的最新值.Volatile变量可用于提供线程安全,但是只能应用于非常 ...

  3. Java并发编程:volatile关键字解析

    Java并发编程:volatile关键字解析 volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在 ...

  4. 从根源上解析 Java volatile 关键字的实现

    1.解析概览 内存模型的相关概念 并发编程中的三个概念 Java内存模型 深入剖析Volatile关键字 使用volatile关键字的场景 2.内存模型的相关概念 缓存一致性问题.通常称这种被多个线程 ...

  5. 白话讲述Java中volatile关键字

    一.由一段代码引出的问题 首先我们先来看这样一段代码: public class VolatileThread implements Runnable{ private boolean flag = ...

  6. 谈谈volatile关键字以及常见的误解

    转载请保留以下声明 作者:赵宗晟 出处:https://www.cnblogs.com/zhao-zongsheng/p/9092520.html 近期看到C++标准中对volatile关键字的定义, ...

  7. 并发编程(一)—— volatile关键字和 atomic包

    本文将讲解volatile关键字和 atomic包,为什么放到一起讲呢,主要是因为这两个可以解决并发编程中的原子性.可见性.有序性,让我们一起来看看吧. Java内存模型 JMM(java内存模型) ...

  8. 全面理解Java内存模型(JMM)及volatile关键字(转载)

    关联文章: 深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解Java类加载器(ClassLoad ...

  9. 全面理解Java内存模型(JMM)及volatile关键字

    [版权申明]未经博主同意,谢绝转载!(请尊重原创,博主保留追究权) http://blog.csdn.net/javazejian/article/details/72772461 出自[zejian ...

随机推荐

  1. 一次神奇的Azure speech to text rest api之旅

    错误Max retries exceeded with url: requests.exceptions.ConnectionError: HTTPSConnectionPool(host='%20e ...

  2. cesium加载gltf模型点击以及列表点击定位弹窗

    前言 cesium 官网的api文档介绍地址cesium官网api,里面详细的介绍 cesium 各个类的介绍,还有就是在线例子:cesium 官网在线例子,这个也是学习 cesium 的好素材. 之 ...

  3. StringBuffer类(增删改查及长度可变原理)

    1 package cn.itcast.p2.stringbuffer.demo; 2 3 public class StringBufferDemo { 4 5 public static void ...

  4. 「JOISC 2014 Day1」巴士走读

    「JOISC 2014 Day1」巴士走读 将询问离线下来. 从终点出发到起点. 由于在每个点(除了终点)的时间被过来的边固定,因此如果一个点不被新的边更新,是不会发生变化的. 因此可以按照时间顺序, ...

  5. AT2645 [ARC076D] Exhausted?

    解法一 引理:令一个二分图两部分别为 \(X, Y(|X| \le |Y|)\),若其存在完美匹配当且仅当 \(\forall S \subseteq X, f(S) \ge |S|\)(其中 \(f ...

  6. JavaCV的摄像头实战之六:保存为mp4文件(有声音)

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  7. 如何高效地写 Form

    工作少不了写"增删改查","增删改查"中的"增"和"改"都与 Form 有关,可以说:提升了 Form 的开发效率,就提 ...

  8. Java线程池实现原理及其在美团业务中的实践(转)

    转自美团技术团队:https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html 随着计算机行业的飞速发展,摩尔定律逐 ...

  9. docker简介及安装(1)

    Docker简介 软件开发中最为麻烦的事情可能就是配置环境了.由于用户使用的操作系统具有多样性,即便使用跨平台的开发语言(如Java和Python)都不能保证代码能够在各种平台下都可以正常的运转,而且 ...

  10. JavaBean基本概念

    JavaBean 是特殊的 Java 类,使用 Java 语言书写,并且遵守 JavaBean API 规范. JavaBean 与其它 Java 类相比而言独一无二的特征: 提供一个默认的无参构造函 ...