C#中的原子操作Interlocked,你真的了解吗?

如果这是第二次看到我的文章,欢迎订阅z哥的公众号(跨界架构师)哦~

每周五11:45 按时送达。当然了,也会时不时加个餐~

一、背景

这个标题起的有点标题党的嫌疑

,这个事情的原委是这样的,有个Web API的站点在本地使用Release模式Run的时候出现问题,但是使用Debug模式则不会。通过打日志定位到问题在如下的这个代码这里:

private  static  int  _flag;

public  void  ExactlyOnceMethod()        

{

        varoriginal = Interlocked.Exchange(ref_flag,1);

        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,1);

00DC35EF  move  cx,5F2DFCCh  //将5F2DFCCh地址上的数据放入寄存器ecx

00DC35F4  move  dx,1//将1放入寄存器edx

00DC35F9  call     70B95330//调用70B95330地址上的方法

00DC35FE  movd  word 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 ptrds:[5F2DFCCh]   //比较地址ds:[5F2DFCCh]的双字型指针上的数据和寄存器eax中的数据。 这里开始下面的代码不是我们的讨论点了,就不翻译了

00DC3610  set       neal

00DC3613  movzx  eax,al

00DC3616  mov      dword ptr [ebp-44h],eax

00DC3619  cmp      dword ptr [ebp-44h],0

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(个人微信号:Zachary-ZF)

微信公众号(首发):跨界架构师<-- 点击查阅近期热门文章

如果你是初级程序员,想提升但不知道如何下手。又或者做程序员多年,陷入了一些瓶颈想拓宽一下视野。欢迎关注我的公众号「跨界架构师」,回复「技术」,送你一份我长期收集和整理的思维导图。

如果你是运营,面对不断变化的市场束手无策。又或者想了解主流的运营策略,以丰富自己的“仓库”。欢迎关注我的公众号「跨界架构师」,回复「运营」,送你一份我长期收集和整理的思维导图。

定期发表原创内容:架构设计丨分布式系统丨产品丨运营丨一些深度思考

扫码加入小圈子👇

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容