一:背景

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.0
IL_0001: stloc.0
IL_0002: br.s IL_0009
// loop start (head: IL_0009)
IL_0004: ldloc.0
IL_0005: ldc.i4.0
IL_0006: ceq
IL_0008: stloc.0 IL_0009: ldarg.0
IL_000a: ldfld bool ConsoleApp1.Worker::_shouldStop
IL_000f: brfalse.s IL_0004
// end loop IL_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.Worker 0:011> !dumpheap -mt 00007ffc8fdd3a90
Address 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之后是取偏移地址上的值比较,这就是真相吧!

四:总结

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

更多高质量干货:参见我的 GitHub: dotnetfly

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

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

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

  2. Java并发专题(三)深入理解volatile关键字

    前言 上一章节简单介绍了线程安全以及最基础的保证线程安全的方法,建议大家手敲代码去体会.这一章会提到volatile关键字,虽然看起来很简单,但是想彻底搞清楚需要具备JMM.CPU缓存模型的知识.不要 ...

  3. 深入理解volatile关键字

    Java内存模型 想要理解volatile为什么能确保可见性,就要先理解Java中的内存模型是什么样的. Java内存模型规定了所有的变量都存储在主内存中.每条线程中还有自己的工作内存,线程的工作内存 ...

  4. 彻底理解volatile关键字

    1. volatile简介 在上一篇文章中我们深入理解了java关键字,我们知道在java中还有一大神器就是关键volatile,可以说是和synchronized各领风骚,其中奥妙,我们来共同探讨下 ...

  5. Java并发编程学习笔记 深入理解volatile关键字的作用

    引言:以前只是看过介绍volatile的文章,对其的理解也只是停留在理论的层面上,由于最近在项目当中用到了关于并发方面的技术,所以下定决心深入研究一下java并发方面的知识.网上关于volatile的 ...

  6. 【java并发】(1)深入理解volatile关键字

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

  7. 【Java并发编程】从CPU缓存模型到JMM来理解volatile关键字

    目录 并发编程三大特性 原子性 可见性 有序性 CPU缓存模型是什么 高速缓存为何出现? 缓存一致性问题 如何解决缓存不一致 JMM内存模型是什么 JMM的规定 Java对三大特性的保证 原子性 可见 ...

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

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

  9. 深入理解 volatile 关键字

    volatile 关键字是 Java 语言的高级特性,但要弄清楚其工作原理,需要先弄懂 Java 内存模型.如果你之前没了解过 Java 内存模型,那可以先看看之前我写过的一篇「深入理解 Java 内 ...

随机推荐

  1. PHP代码审计01之in_array()函数缺陷

    前言 从今天起,结合红日安全写的文章,开始学习代码审计,题目均来自PHP SECURITY CALENDAR 2017,讲完这个题目,会再用一道有相同问题的CTF题来进行巩固.下面开始分析. 漏洞分析 ...

  2. Ubuntu中发生git Connection refused

    今天在提交代码的时候: ssh: connect to host github.com port 22: Connection refused fatal: 无法读取远程仓库. 请确认您有正确的访问权 ...

  3. Restful 风格是什么?

    1.1 什么是RESTful 1. REST与技术无关,代表的是一种软件架构风格(REST是Representational State Transfer的简称,中文翻译为"表征状态转移&q ...

  4. 实验 3:Mininet 实验——测量路径的损耗率

    一.实验目的 在实验 2 的基础上进一步熟悉 Mininet 自定义拓扑脚本,以及与损耗率相关的设定;初步了解 Mininet 安装时自带的 POX 控制器脚本编写,测试路径损耗率. 二.实验任务 h ...

  5. C++库文件解析(conio.h)

    转载:https://blog.csdn.net/ykmzy/article/details/51276596 Conio.h 控制台输入输出库该文内容部分参照百度百科 Conio.h 在C stan ...

  6. MySql查询语句中的变量使用

    前言 今日在LeetCode刷MySql的题,遇到一题,题目到没什么,解答完了之后习惯去看此题的题解,有位大佬的思路让博主感觉很惊艳,至此,特地记录学习一下. 题目 解答 乍一看题目也没啥,分数排名, ...

  7. CSS字体属性与文本属性

    CSS字体属性与文本属性 1. 字体属性 1.1 字体系列font-family p { font-family: "Microsoft Yahei";/*微软雅黑*/ } /*当 ...

  8. web中的HTML CSS

    HTML  超文本标记语言      页面的结构  首行声明文档类型>根标记>头部标记/主体标记      标记         div 布局 切割 分割 划分区域         spa ...

  9. Oracle使用技巧

    Edit/Undo Ctrl+ZEdit/Redo Shift+Ctrl+ZEdit/PL/SQL Beautifier Ctrl+W (自定义) Shift+Home 选择光标位置到行首 Shift ...

  10. 【全网免费VIP观看】哔哩哔哩番剧解锁大会员-集合了优酷-爱奇艺-腾讯-芒果-乐视-ab站等全网vip视频免费破解去广告-高清普清电视观看-持续更新

    哔哩哔哩番剧解锁大会员-集合了优酷-爱奇艺-腾讯-芒果-乐视-ab站等全网vip视频免费破解去广告-高清普清电视观看-持续更新 前言 突然想看电视,结果 没有VIP 又不想花钱,这免费的不久来啦. 示 ...