C#中的原子操作Interlocked,你真的了解吗?
阅读目录
一、背景
这个标题起的有点标题党的嫌疑[捂脸],这个事情的原委是这样的,有个Web API的站点在本地使用Release模式Run的时候出现问题,但是使用Debug模式则不会。通过打日志定位到问题在如下的这个代码这里:
private static int _flag; public void ExactlyOnceMethod()
{
var original = Interlocked.Exchange(ref _flag, );
if (original == _flag)
{
// 1.重复进入
}
else
{
// 2.第一次进入
}
}
理论上,会有一次请求进入到2中,但是实际问题是全部都进入到了1中。
二、代码描述
这个代码很简单,就做了2个事情,1是使用Interlocked.Exchange将_flag变量进行赋值。2是将Interlocked.Exchange操作后返回的原始值与_flag变量进行对比,如果相等说明这个变量已经被修改过了,表示这里是重入了。如果不是则说明第一次进入此方法。
关于Interlocked.Exchange的解释,见微软官网文档,传送门在此:https://msdn.microsoft.com/zh-cn/library/d3fxt78a.aspx
三、越分析越黑暗
好了,咋一看了好几分钟,也没看出有什么不妥的地方,那么首先就往多线程问题上考虑了。但是这里唯一的共享变量就是_flag,走的又是CAS操作,在这里不存在多线程问题。而且结合日志输出,的确这个方法就是只执行了一次。仔细的再看了一遍官方文档中的内容,见下图1。我发现示例代码中的写法和我上面贴的代码是不一样的,这里并没有重用变量usingResource,而且直接将比较的对象变成了一个常量0。
【图1】
带着好奇,我去翻阅了下.Net Framework的源码。传送门在此 http://referencesource.microsoft.com/#mscorlib/system/threading/interlocked.cs,52be0cc9b3954ae9 。但是它直接是个extern方法,见下图2:
【图2】
这里又陷入了一个困境,现在线索断了。查阅了一些资料,MethodImplOptions.InternalCall 表明这个方法的实现在微软开源的sscli中可以找到答案(原文地址 http://bbs.csdn.net/topics/330019064 中的5楼回复)。但是经各方查找,目前已经找不到源码所在地了,据说是.Net Framework 2.0时代的产物。
OK,那我就想看下汇编代码试试。下面是反编译出的汇编代码:
var original = Interlocked.Exchange(ref _flag, );
00DC35EF mov ecx,5F2DFCCh //将5F2DFCCh地址上的数据放入寄存器ecx
00DC35F4 mov edx, //将1放入寄存器edx
00DC35F9 call 70B95330 //调用70B95330地址上的方法
00DC35FE mov dword ptr [ebp-48h],eax //将寄存器eax的数据 保存到地址ebp-48h的双字型指针上
00DC3601 mov eax,dword ptr [ebp-48h] //将地址ebp-48h的双字型指针上的数据放入寄存器eax(可以理解上上一步的反向操作)
00DC3604 mov dword ptr [ebp-40h],eax //将寄存器eax的数据 保存到地址ebp-40h的双字型指针上
if (original == _flag)
00DC3607 mov eax,dword ptr [ebp-40h] //将地址ebp-40h的双字型指针上的数据放入寄存器eax
00DC360A cmp eax,dword ptr ds:[5F2DFCCh] //比较地址ds:[5F2DFCCh]的双字型指针上的数据和寄存器eax中的数据。 这里开始下面的代码不是我们的讨论点了,就不翻译了
00DC3610 setne al
00DC3613 movzx eax,al
00DC3616 mov dword ptr [ebp-44h],eax
00DC3619 cmp dword ptr [ebp-44h],
00DC361D jne 00DC3624
这里的5F2DFCCh其实就是_flag。我们可以看到在真正做这个Interlocked.Exchange操作的时候,并没有直接去修改5F2DFCCh地址上的数据,但是在做cmp操作的时候由于我们比较的对象是_flag变量,所以还是继续使用了5F2DFCCh地址上的数据。也就是说:CPU运算在寄存器中操作数据,但是我们用于判断的变量是个静态全局变量,持有的是这个引用地址。那么是不是可以这么来理解:【如果说Interlocked的内部操作与当前上下文使用的并不是同一个CPU核心】,那么这个“判断依据”并不是像代码上写的这样,因为我们预期是肯定一样的(变量都是同一个)。理由是做Interlocked的时候在CPU1的高速缓存中,另一个在CPU2上操作加载的数据还是内存中的。其中CPU1往内存同步数据(将寄存器中的值赋值给_flag这个全局变量)有一个非常短的时间差。如果是这样的话,也就能解释为什么会有下面的3种情况出现:
1.在有的机器上是没问题的,在有的机器上是有问题的。
2.在Debug模式下是没问题的,在Release模式下是有问题的。
3.在if语句之前增加一条日志记录到物理文件中也是没问题的。
依据这个推测的话,原因就是因为这个时间差的耗时和所在机器的硬件配置环境都有关系。只要这个“赋值”操作所用时间 < 代码执行到if所需的时间,那么就不会出现问题。根据这个结论也能得出解决方案,就是让这个表达式成立即可,哪怕就是简单粗暴的Sleep1毫秒都行。笔者建议的解决方案有2种:
方案1:是给这个全局变量增加volatile关键字即可,关键字的说明请看这里(https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/volatile)。
方案2:参照官方的示例写法,将_flag替换为常量来做比较,比如这里可以更改成original == 0 即可。
四、结语
总结一下:
使用Interlocked做的CAS本身是一个CPU操作。数据是放在CPU的寄存器中做的交换。但是我们判断的变量是个静态全局变量,持有的是这个引用地址。
也就是出现问题的流程是:
1.从传入的ref引用地址加载数据到CPU寄存器
2.寄存器中做交换并且返回原始值,但是更新引用地址的操作并不是在这个上下文中的同步操作。
3.然后我们比较的时候,左侧原始值肯定为0,但是流程1中的变量在非常短的时间内也是原始值为0(如图3)。导致了这个问题的产生。
【图3】
强调一下,这个结论也是建立在【如果说Interlocked的内部操作与当前上下文使用的并不是同一个CPU核心】的猜测下,这方面资料实在是找不到也无法进一步验证,所以我也不是敢100%确定是否正确。如果哪位小伙伴能够来个明确的解惑欢迎在下面留言~
在分析该问题的过程中,参考了以下几位小伙伴的思想成果,感谢分享:
http://286.iteye.com/blog/2295165
http://www.cnblogs.com/5iedu/p/4719625.html
http://blog.csdn.net/hsuxu/article/details/9467651
作者:Zachary
出处:https://zacharyfan.com/archives/262.html
▶关于作者:张帆(Zachary,个人微信号:Zachary-ZF)。坚持用心打磨每一篇高质量原创。欢迎扫描右侧的二维码~。
定期发表原创内容:架构设计丨分布式系统丨产品丨运营丨一些思考。
如果你是初级程序员,想提升但不知道如何下手。又或者做程序员多年,陷入了一些瓶颈想拓宽一下视野。欢迎关注我的公众号「跨界架构师」,回复「技术」,送你一份我长期收集和整理的思维导图。
如果你是运营,面对不断变化的市场束手无策。又或者想了解主流的运营策略,以丰富自己的“仓库”。欢迎关注我的公众号「跨界架构师」,回复「运营」,送你一份我长期收集和整理的思维导图。
C#中的原子操作Interlocked,你真的了解吗?的更多相关文章
- 原子操作 Interlocked系列函数
上一篇<多线程第一次亲密接触 CreateThread与_beginthreadex本质区别>中讲到一个多线程报数功能.为了描述方便和代码简洁起见,我们可以只输出最后的报数结果来观察程序是 ...
- (转)原子操作 Interlocked系列函数
上一篇<多线程第一次亲密接触 CreateThread与_beginthreadex本质区别>中讲到一个多线程报数功能.为了描述方便和代码简洁起见,我们可以只输出最后的报数结果来观察程序是 ...
- 多线程面试题系列(3):原子操作 Interlocked系列函数
上一篇中讲到一个多线程报数功能.为了描述方便和代码简洁起见,我们可以只输出最后的报数结果来观察程序是否运行出错.这也非常类似于统计一个网站每天有多少用户登录,每个用户登录用一个线程模拟,线程运行时会将 ...
- 秒杀多线程第三篇 原子操作 Interlocked系列函数
上一篇<多线程第一次亲密接触 CreateThread与_beginthreadex本质区别>中讲到一个多线程报数功能.为了描述方便和代码简洁起见,我们可以只输出最后的报数结果来观察程序是 ...
- 多线程--原子操作 Interlocked系列函数
[转]原文地址:http://blog.csdn.net/morewindows/article/details/7429155 线程同步与互斥: 互斥主要指多个线程不能同时访问一个资源,如打印机就是 ...
- Java学习技术分享:Java中的原子操作
学习java需要有一套完整的学习线路,需要有条理性,当下学习java已经有一段时间了,由当初的懵逼状态逐渐好转,也逐渐养成了写技术学习笔记的习惯,今天总结了一下java中的原子操作. 1.Java中的 ...
- OpenGL中的原子操作需要注意的地方
OpenGL中的原子操作需要注意的地方 仔细阅读看画红线的部分
- Java中的原子操作类
转载: <ava并发编程的艺术>第7章 当程序更新一个变量时,如果多线程同时更新这个变量,可能得到期望之外的值,比如变量i=1,A线程更新i+1,B线程也更新i+1,经过两个线程操作之后可 ...
- 【Java并发】Java中的原子操作类
综述 JDK从1.5开始提供了java.util.concurrent.atomic包. 通过包中的原子操作类能够线程安全地更新一个变量. 包含4种类型的原子更新方式:基本类型.数组.引用.对象中字段 ...
随机推荐
- Azure Powershell对ARM资源的基本操作
本分主要介绍Windows Azure Powershell对ARM资源的基本操作 1.登陆ARM模式,命令:Login-AzureRmAccount -EnvironmentName AzureCh ...
- C# 反向生成工具(DAL BLL Modle)
VS2015 ADO.NET无果后果断~! 动软生成:http://pan.baidu.com/s/1gfIf0ZL
- java--利用exe4j生成.exe的可执行文件
工具:eclipse,exe4j,jre,这三个都可以直接在官方网站下载,下面所用到的都是最新版的. 前期准备:用eclipse编好需要生成.exe文件的project,另外exe4j需要一个注册码, ...
- 团队作业4——第一次项目冲刺(Alpha版本)4.22
团队作业4--第一次项目冲刺(Alpha版本) Day one: 会议照片 由于团队中的组员今天不在学校,所以我们的站立会议提前一天展开. 项目进展 由于今天是Alpha版本项目冲刺的第一天,所以没有 ...
- 201521123012 《Java程序设计》第五周学习总结
##1. 本周学习总结 1.1 尝试使用思维导图总结有关多态与接口的知识点. 答: 1.2 可选:使用常规方法总结其他上课内容. 答:匿名内部类:将一个类的定义放在另一个类的内部.一般是 **new ...
- 201521123090 《Java程序设计》第5周学习总结
1. 本周学习总结 1.1 尝试使用思维导图总结有关多态与接口的知识点. 1.2 可选:使用常规方法总结其他上课内容. 参考资料: 2. 书面作业 作业参考文件下载 1.代码阅读:Child压缩包内源 ...
- 201521123005 《java程序设计》 第二周学习总结
1. 本周学习总结 ·java中的字符串及String的用法 "=="比较的是两字符串的地址,而不是内容 String类的对象是不可变的,创建之后不能进行修改 ·数组Array的用 ...
- 201521123036 《Java程序设计》第12周学习总结
本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结多流与文件相关内容. 书面作业 将Student对象(属性:int id, String name,int age,double grad ...
- Vagrant下共享目录下静态文件(js/jpg/png等)修改完运行报错
利用Vagrant部署开发环境,使用目录共享模式,在本地磁盘进行开发,通过虚拟机环境运行开发的页面. 接下来打开页面,看上去一切正常,接下来将发生一个神奇的事情,你修改一个css文件,在刷新浏览器,发 ...
- Spring第一篇【介绍Spring、引入Spring、Spring六大模块】
前言 前面已经学习了Struts2和Hibernate框架了.接下来学习的是Spring框架-本博文主要是引入Spring框架- Spring介绍 Spring诞生: 创建Spring的目的就是用来替 ...