一:背景

1. 讲故事

昨天在园里的编辑头条看到 精致码农大佬 写的一篇题为:[C#.NET 拾遗补漏]10:理解 volatile 关键字 (https://www.cnblogs.com/willick/p/13889006.html) 的文章,大概就是说在 多线程环境下,一个在debug不出现,在release中出现的bug,原文代码如下:


public class Worker
{private bool _shouldStop;public void DoWork(){bool work = false;// 注意:这里会被编译器优化为 while(true)while (!_shouldStop){work = !work; // do sth.}Console.WriteLine("工作线程:正在终止...");}public void RequestStop(){_shouldStop = true;}
}public class Program
{public static void Main(){var worker = new Worker();Console.WriteLine("主线程:启动工作线程...");var workerTask = Task.Run(worker.DoWork);// 等待 500 毫秒以确保工作线程已在执行Thread.Sleep(500);Console.WriteLine("主线程:请求终止工作线程...");worker.RequestStop();// 待待工作线程执行结束workerTask.Wait();//workerThread.Join();Console.WriteLine("主线程:工作线程已终止");}
}

文中分析这个bug是因为在 release 环境下,jit做了 while (!_shouldStop) -> while(true) 的代码优化。

2. 我的质疑

为什么我对这个问题比较敏感呢?第一:这是一个经典的问题,第二:我在 2017-03-20 也写过一篇这样的文章:享受release版本发布的好处的同时也应该警惕release可能给你引入一些莫名其妙的大bug  (https://www.cnblogs.com/huangxincheng/p/6585907.html) ,那篇文章我分析是因为 cpu缓存 和 内存 两者之间不一致导致的脏读,显然和大佬的结论大相径庭,而且两篇文章都存在一个问题,就是草率的下结论,并没有拿出一个完整的证据链来证明真的是这样, 这篇文章的目的就是试着拿出我认为的证据链。

二:真的被优化为 while(true) 了吗

1. 从两次编译阶段中寻找答案

大家应该都知道代码会经历两个阶段的编译:第一阶段:编译器会把 C# code 编译成 MSIL 代码 ,第二阶段:CLR 会启动 JIT 将 MSIL 编译成机器代码,画一张图如下:

既然大佬说被优化成 while(true) 了,那意思就是说要么在 MSIL 中被优化,要么在 机器码 中被优化,这里我可以用 ILSpy 和 Windbg 去挖一挖,看看大佬说的是否正确?

2. 用 ILSpy 查看 MSIL 是否被优化

把项目编译成 release 模式,直接查看 DoWork() 的MSIL,如下所示:


.method public hidebysig instance void DoWork () cil managed
{// Method begins at RVA 0x2048// Code size 28 (0x1c).maxstack 2.locals init ([0] bool work)IL_0000: ldc.i4.0IL_0001: stloc.0IL_0002: br.s IL_0009// loop start (head: IL_0009)IL_0004: ldloc.0IL_0005: ldc.i4.0IL_0006: ceqIL_0008: stloc.0IL_0009: ldarg.0IL_000a: ldfld bool ConsoleApp1.Worker::_shouldStopIL_000f: brfalse.s IL_0004// end loopIL_0011: ldstr "工作线程:正在终止..."IL_0016: call void [System.Console]System.Console::WriteLine(string)IL_001b: ret
} // end of method Worker::DoWork

从这句:ldfld bool ConsoleApp1.Worker::_shouldStop 可看出,代码并没有做任何优化,有点遗憾继续看看第二阶段。

3. 使用 windbg 查看 机器码 是否被优化

很显然机器码给大家看也看不懂,只能看被 JIT 编译成 机器代码 的 汇编代码,废话不多说,生成一个 dump 文件.

  • 用 name2ee 查看 DoWork 的方法描述符


0:011> !name2ee ConsoleApp1!Worker.DoWork
Module:      00007ffc8fdaf7e0
Assembly:    ConsoleApp1.dll
Token:       0000000006000001
MethodDesc:  00007ffc8fdd3a50
Name:        ConsoleApp1.Worker.DoWork()
JITTED Code Address: 00007ffc8fd17500

从 JITTED Code Address: 00007ffc8fd17500 可以看到,DoWork 已经被 JIT 编译过了,好事情。

  • 用 !U 查看 DoWork 的反汇编

对照代码图可以看到

  • ecx 寄存器 存放着 _shouldStop 值.

  • eax 寄存器 存放着 work  值

既然有两个寄存器存放着两个值,也就说明  while (!_shouldStop) -> while(true) 这个说法是站不住脚的。。。那真相是什么呢?我试着揭晓。

三:我所谓的真相

1. 验证寄存器的值

很明显当前的程序正在死循环,说明_shouldStop变量此时肯定是false,为了验证是否正确,通过 r 命令查看一下此时寄存器的值。


0:011> r ecx
ecx=0

2. 验证内存中的 _shouldStop 的值

要想验证内存中的 _shouldStop 是否已经为 true,最简单的办法就是去 托管堆 找 Work 对象,看看它的实例变量 _shouldStop 是否为 true 即可。


0:011> !dumpheap -stat
Statistics:MT    Count    TotalSize Class Name
00007ffc8fdd3a90        1           24 ConsoleApp1.Worker0:011> !dumpheap -mt 00007ffc8fdd3a90Address               MT     Size
000001ee59f4abd8 00007ffc8fdd3a90       24     0:011> !do 000001ee59f4abd8
Name:        ConsoleApp1.Worker
MethodTable: 00007ffc8fdd3a90
EEClass:     00007ffc8fdccda8
Size:        24(0x18) bytes
File:        E:\net5\ConsoleApp1\ConsoleApp1\bin\x64\Release\netcoreapp3.1\ConsoleApp1.dll
Fields:MT    Field   Offset                 Type VT     Attr            Value Name
00007ffc8fcd71d0  4000001        8       System.Boolean  1 instance                1 _shouldStop

从最后一行代码可以看到:_shouldStop =1 , 证明内存中的 _shouldStop 确实为 true,没毛病!

3. 整体思路

到这里是不是已经非常清晰了,由于while循环太频繁了,release做了代码优化,将 _shouldStop 的值直接放在了 ecx 寄存器中, 当B线程执行 _shouldStop=true 更新到内存的时候,并没有什么通知机制,导致A线程在不知情的情况下一直读自己的 ecx 寄存器的值0,这时候就脏读了,脑子里是不是有一张蓝图?大概就像下面这样:

思想知道了,解决这个问题也就简单了,给 _shouldStop 打上 volatile 标记,让cpu每次都到内存中取 _shouldStop 值即可,


private volatile bool _shouldStop;

然后再看 Dowork 的反汇编:

为了更加可视化,来张对比图,很明显可以看到, volatile之前是直接取值比较,volatile之后是取偏移地址上的值比较,这就是真相吧!

四:总结

总的来说还是脏读引起的问题,刚好也补充了之前文章未寻找真相的一个遗憾吧,也感谢 精致码农大佬 原创输出。

对精致码农大佬的 [理解 volatile 关键字] 文章结论的思考和寻找真相相关推荐

  1. 对 精致码农大佬 说的 Task.Run 会存在 内存泄漏 的思考

    一:背景 1. 讲故事 这段时间项目延期,加班比较厉害,博客就稍微停了停,不过还是得持续的技术输出呀!园子里最近挺热闹的,精致码农大佬分享了三篇文章: 为什么要小心使用 Task.Run   [htt ...

  2. java 调用dll内存泄露_对 精致码农大佬 说的 Task.Run 会存在 内存泄漏 的思考

    一:背景 1. 讲故事 这段时间项目延期,加班比较厉害,博客就稍微停了停,不过还是得持续的技术输出呀!园子里最近挺热闹的,精致码农大佬分享了三篇文章: 为什么要小心使用 Task.Run   [htt ...

  3. qthread run结束了算销毁吗_对 精致码农大佬 说的 Task.Run 会存在 内存泄漏 的思考...

    一:背景 1. 讲故事 这段时间项目延期,加班比较厉害,博客就稍微停了停,不过还是得持续的技术输出呀! 园子里最近挺热闹的,精致码农大佬分享了三篇文章: 为什么要小心使用 Task.Run [http ...

  4. 有关Quartz.NET,与一线码农大佬对个线?

    跟[一线码农大佬]翻译的某技术文对个线 最近看到一线码农大佬翻译的<如何在 ASP.NET Core 中使用 Quartz.NET 执行任务调度>, 行文思路: 安装Quartz.NET ...

  5. [C#.NET 拾遗补漏]10:理解 volatile 关键字

    要理解 C# 中的 volatile 关键字,就要先知道编译器背后的一个基本优化原理.比如对于下面这段代码: public class Example {public int x;public voi ...

  6. 【Java线程】深入理解Volatile关键字和使用

    目录 背景 volatile原理 volatile特性 可见性 有序性 原子性 使用场景 背景 理解volatile底层原理之前,首先介绍关于缓存一致性协议的知识. 背景:计算机在执行程序时,每条指令 ...

  7. 深入理解volatile关键字---缓存一致性原理

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

  8. 理解volatile关键字

    一. volatile的作用 1. 可见性 cpu 在变量赋值之后加上写屏障,使得对 volatile变量 以及之前变量的写都写入到主内存中 cpu 在变量读取之前加上读屏障,使得对 volatile ...

  9. 码农翻身讲网络3:从Socket编程到HTTP服务器

    小白科普:从输入网址到最后浏览器呈现页面内容,中间发生了什么?(HTTP请求) 原创: 老刘 码农翻身 1月2日 1前言 这篇文章是应网友之邀所写,主要描述一下我们访问网站时, 从输入网址到最后浏览器 ...

最新文章

  1. ecside 列表排序问题
  2. 恢复Win10系统默认内置应用
  3. 关于ANSI和UTF-8,windows和unix的行结束符
  4. js 正则 或者_正则表达式
  5. android播放mp3方法,Android之MediaPlayer播放音频与视频
  6. 修改了系统时间后,myeclipse 和tomcat下的代码不同步了
  7. 人工智能红利渗透与爆发
  8. Python 字符串语法,for
  9. 再谈谈ADO.NET Data Service
  10. android常用的存储方式,Android数据的四种存储方式
  11. 家用智能门锁常见的开锁方式
  12. 基于STM32设计的智能家居系统(采用ESP8266+OneNet云平台)
  13. c语言的三个基本语句,C语言-桂林理工大学3-第三章 C程序设计的基本语句.doc
  14. 汇智动力软件测试问题,汇智动力—测试工程师都是怎么写测试用例的?
  15. 外推法的matlab程序
  16. 图像超分辨率论文笔记
  17. 浙江大学计算机2020分数线,2021年浙江大学录取分数线(含2019-2020分数线)
  18. matlab怎么求三次微分,Matlab – 求解三阶微分方程
  19. NFS服务器的配置与管理
  20. background多背景

热门文章

  1. Flask博客开发——Tinymce编辑器
  2. [ 转载 ] Java面试精选【Java基础第一部分】
  3. [BZOJ]1095 Hide捉迷藏(ZJOI2007)
  4. 15_新闻客户端_展示文字内容完成
  5. C#网络编程(订立协议和发送文件) - Part.4
  6. ecshop在首页调用dedecms文章
  7. UVA 10518 How Many Calls?
  8. [导入]Asp.net中动态在中加入Scrpit标签
  9. 固件中启用的虚拟化否_哪些固件或硬件机制可启用强制关机?
  10. java 多线程 优先级_java多线程之线程的优先级