volatile :适用于多线程的情况,因为单线程的代码如果被编译器优化了,是不会出现问题的。
单线程是串行的,在线程方法外修改字段的值是不会有影响的。
如果是多线程在线程方法外修改了这个线程内的某个值,是会影响到这个方法的执行的(但此时如果代码被编译器优化了,那代码执行时)
使用Volatile对字段操作,总是直接对RAM进行操作,而不会对CPU寄存器进行操作
ConcurrentQueue<T> 源码发现里面没有用到lock,ConcurrentDictionary里面是有lock的,lock的是字典里面每一个key,但是ConcurrentQueue<T> 的线程安全确是用SpinWait对象和volatile关键字来实现。
//用户模式-同步机制(轮训CPU,不用上下文切换,合适等待时间短的操作)==============不切换上下文=====消耗CPU //内核模式构造(需要上下文切换、消耗操作系统资源)===============切换上下文======消耗系统资源 static void Main(string[] args) { var t1 = new Thread(UserModeWait); var t2 = new Thread(HybridSpinWait); Console.WriteLine("运行用户模式 waiting"); t1.Start(); Thread.Sleep(20); //线程1一直循环,直到这里(_isCompleted为True退出循环) _isCompleted = true;//(加了这个关键字volatile,UserModeWait方法才能感知到这里进行了修改) Thread.Sleep(TimeSpan.FromSeconds(1)); _isCompleted = false;//修改为false准备为下面的循环作准备 Console.WriteLine("运行混合的 SpinWait 构造 waiting"); t2.Start();//开始混合模式 Thread.Sleep(5);//保持运行5毫秒 _isCompleted = true; Console.Read(); } //volatile指出这个字段可能被多个线程修改 //禁用、防止编译器的一些优化为只能被单个线程访问。确保总是访问该字段总是最新值。看下面第5行解释。 //换句话说,使用Volatile对字段操作,总是直接对RAM进行操作,而不会对CPU寄存器进行操作(不使用CPU寄存器,速度肯定会比使用CPU寄存器慢)) static volatile bool _isCompleted = false; /* volatile多用于多线程的环境,当一个变量定义为volatile时,读取这个变量的值时候每次都是从momery里面读取而不是从cache读。 这样做是为了保证读取该变量的信息都是最新的,而无论其他线程如何更新这个变量。 volatile 修饰符通常用于由多个线程访问但不使用 lock 语句对访问进行序列化的字段。 volatile 关键字可应用于以下类型的字段: 引用类型。 指针类型(在不安全的上下文中)。 请注意,虽然指针本身可以是可变的,但是它指向的对象不能是可变的。 换句话说,您无法声明“指向可变对象的指针”。 类型,如 sbyte、byte、short、ushort、int、uint、char、float 和 bool。 具有以下基类型之一的枚举类型:byte、sbyte、short、ushort、int 或 uint。 已知为引用类型的泛型类型参数。 IntPtr 和 UIntPtr。 可变关键字仅可应用于类或结构字段。 不能将局部变量声明为 volatile。 */ //字段如果不加volatile,并且启用编译器优化,UserModeWait()可能会一直运行 //每次当while(!s_stopWorker)执行时,并不会去检查s_stopWorker的真实值,只检查一次(也就是在循环开始的时候),之后其会记住s_stopWorker的值,本例中为false,所以程序会一直执行 static void UserModeWait() { while (!_isCompleted)//用户模式,会不断访问CPU,任务管理器中查看 ,CPU会显示一个显著的处理时间 { Console.Write("."); } Console.WriteLine(); Console.WriteLine("Waiting is complete"); } static void HybridSpinWait() { /* 自旋等待 一个轻量同步类型(结构体),提供对基于自旋的等待的支持。SpinWait只有在多核处理器下才具有使用意义。在单处理器下,自旋转会占据CPU时间,却做不了任何事。 SpinWait并没有设计为让多个任务或线程并发使用。因此,如果多个任务或者线程通过SpinWait的方法进行自旋,那么每一个任务或线程都应该使用自己的SpinWait实例。 */ var w = new SpinWait(); while (!_isCompleted)//前几个循环会用用户模式,9个迭代后开始切换到内核模式(阻塞状态)【CPU不会有任何负载,因为线程已经阻塞(一边歇着去了)】 { // 执行单一自旋。 w.SpinOnce(); /* 判断对SpinWait.SpinOnce() 的下一次调用是否触发上下文切换和内核转换。 由NextSpinWillYield属性代码可知,若SpinWait运行在单核计算机上,它总是进行上下文切换(让出处理器)。 SpinWait不仅仅是一个空循环。它经过了精心实现,可以针对一般情况提供正确的旋转行为以避免内核事件所需的高开销的上下文切换和内核转换; 在旋转时间足够长的情况下自行启动上下文切换,SpinWait甚至还会在多核计算机上产生线程的时间片(Thread.Yield())以防止等待线程阻塞高优先级的线程或垃圾回收器线程。 */ Console.WriteLine("是否触发上下文切换和内核转换:" + w.NextSpinWillYield);
//NextSpinWillYiel 返回对System.Threading.SpinWait.SpinOnce() 的下一次调用是否将产生处理器,同时触发强制上下文切换(其实就是是否切换到内核模式) } Console.WriteLine("Waiting is complete"); }
工作原理:
当主程序启动时,定义了一个线程,将执行一个无止境的循环,知道20毫秒后主线程设置 _isCompleted变量为true。我们可以试验运行该周期为20~30秒,通过Windows任务管理器测量CPU的负载情况。取决于CPU内核数量,任务管理器将显示一个显著的处理时间。
我们使用volatile关键字来声明_isCompleted静态字段。Volatile关键字指出一个字段可能会被同事执行的多个线程修改。声明为volatile的字段不会被编译器和处理器优化为只能被单个线程访问。这确保了该字段总是最新的值。
然后我们使用了SpinWait版本,用于在每个迭代打印一个特殊标志位来显示线程是否切换为阻塞状态。运行该线程5毫秒来查看结果。刚开始,SpinWait尝试使用用户模式,在9个迭代后,开始切换线程为阻塞状态。如果尝试测量该版本的CPU负载,在Window任务管理器将不会看到任何CPU的使用。
用户模式-同步机制(不切换上下文=====消耗CPU):
适合锁定时间短的操作。(小鸟吃的勤,但吃的时间短,还行。如果来的勤,还吃的时间长,那肯定不行,别的小鸟还吃不吃了。)
内核模式-同步机制(切换上下文======消耗系统资源):
适合锁定时间长的操作。(鸟妈妈脑子不好使,记住喂了哪只很费劲。所以每一只喂时间长些,还行。如果时间短,时间全花在琢磨喂了哪只,没喂哪只上了)
混合模式同步机制:先使用用户模式,几个时钟周期得不到锁,则转换为内核模式。