Csharp/C#教程:深入多线程之:内存栅栏与volatile关键字的使用分析分享

以前我们说过在一些简单的例子中,比如为一个字段赋值或递增该字段,我们需要对线程进行同步,
虽然lock可以满足我们的需要,但是一个竞争锁一定会导致阻塞,然后忍受线程上下文切换和调度的开销,在一些高并发和性能比较关键的地方,这些是不能忍受的。
.netframework提供了非阻塞同步构造,为一些简单的操作提高了性能,它甚至都没有阻塞,暂停,和等待线程。
MemoryBarriersandVolatility(内存栅栏和易失字段)
考虑下下面的代码:
代码如下:
int_answer;
       bool_complete;
       voidA()
       {
           _answer=123;
           _complete=true;
       }
       voidB()
       {
           if(_complete)
               Console.WriteLine(_answer);
       }

如果方法A和B都在不同的线程下并发的执行,方法B可能输出“0”吗?

回答是“yes”,基于以下原因:
   编译器,clr或cpu可能会为了性能而重新为程序的指令进行排序,例如可能会将方法A中的两句代码的顺序进行调整。
   编译器,clr或cpu可能会为变量的赋值采用缓存策略,这样这些变量就不会立即对其他变量可见了,例如方法A中的变量赋值,不会立即刷新到内存中,变量B看到的变量并不是最新的值。
C#和运行时非常小心的保证这些优化策略不会影响正常的单线程的代码和在多线程环境下加锁的代码。
除此之外,你必须显示的通过创建内存屏障(Memoryfences)来限制指令重新排序和读写缓存对程序造成的影响。
Fullfences:

最简单的完全栅栏的方法莫过于使用Thread.MemoryBarrier方法了。
以下是msdn的解释:
Thread.MemoryBarrier:按如下方式同步内存访问:执行当前线程的处理器在对指令重新排序时,不能采用先执行MemoryBarrier调用之后的内存访问,再执行MemoryBarrier调用之前的内存访问的方式。
按照我个人的理解:就是写完数据之后,调用MemoryBarrier,数据就会立即刷新,另外在读取数据之前调用MemoryBarrier可以确保读取的数据是最新的,并且处理器对MemoryBarrier的优化小心处理。
代码如下:
int_answer;
       bool_complete;
       voidA()
       {
           _answer=123;
           Thread.MemoryBarrier();//在写完之后,创建内存栅栏
           _complete=true;
           Thread.MemoryBarrier();//在写完之后,创建内存栅栏      
      }
       voidB()
       {
           Thread.MemoryBarrier();//在读取之前,创建内存栅栏
           if(_complete)
           {
               Thread.MemoryBarrier();//在读取之前,创建内存栅栏
               Console.WriteLine(_answer);
           }
       }

一个完全的栅栏在现代桌面应用程序中,大于需要花费10纳秒。
下面的一些构造都隐式的生成完全栅栏。

   C#Lock语句(Monitor.Enter/Monitor.Exit)
   在Interlocked类的所有方法。
   使用线程池的异步回调,包括异步的委托,APM回调,和Taskcontinuations.
   在一个信号构造中的发送(Settings)和等待(waiting)

你不需要对每一个变量的读写都使用完全栅栏,假设你有三个answer字段,我们仍然可以使用4个栅栏。例如:
代码如下:
int_answer1,_answer2,_answer3;
       bool_complete;
       voidA()
       {
           _answer1=1;_answer2=2;_answer3=3;
           Thread.MemoryBarrier();//在写完之后,创建内存栅栏
           _complete=true;
           Thread.MemoryBarrier();//在写完之后,创建内存栅栏
       }
       voidB()
       {
           Thread.MemoryBarrier();//在读取之前,创建内存栅栏
           if(_complete)
           {
               Thread.MemoryBarrier();//在读取之前,创建内存栅栏
               Console.WriteLine(_answer1+_answer2+_answer3);
           }
       }

我们真的需要lock和内存栅栏吗?
在一个共享可写的字段上不使用lock或者栅栏就是在自找麻烦,在msdn上有很多关于这方面的主题。
考虑下下面的代码:
代码如下:
publicstaticvoidMain()
       {
           boolcomplete=false;
           vart=newThread(()=>
               {
                   booltoggle=false;
                   while(!complete)toggle=!toggle;
               });
           t.Start();
           Thread.Sleep(1000);
           complete=true;
           t.Join();
       }

如果你在VisualStudio中选择发布模式,生成该应用程序,那么如果你直接运行应用程序,程序都不会中止。
因为CPU寄存器把complete变量的值给缓存了。在寄存器中,complete永远都是false。
通过在while循环中插入Thread.MemoryBarrier,或者是在读取complete的时候加锁都可以解决这个问题。
volatile关键字
为_complete字段加上volatile关键字也可以解决这个问题。
volatilebool_complete.
Volatile关键字会指导编译器自动的为读写字段加屏障.以下是msdn的解释:
volatile关键字指示一个字段可以由多个同时执行的线程修改。声明为volatile的字段不受编译器优化(假定由单个线程访问)的限制。这样可以确保该字段在任何时间呈现的都是最新的值。
使用volatile字段可以被上述就是C#学习教程:深入多线程之:内存栅栏与volatile关键字的使用分析分享的全部内容,如果对大家有所用处且需要了解更多关于C#学习教程,希望大家多多关注—计算机技术网(www.ctvol.com)!

本文来自网络收集,不代表计算机技术网立场,如涉及侵权请联系管理员删除。

ctvol管理联系方式QQ:251552304

本文章地址:https://www.ctvol.com/cdevelopment/904729.html

(0)
上一篇 2021年10月22日
下一篇 2021年10月22日

精彩推荐