首页 > 代码库 > C#SpinWait和volatile一点温习
C#SpinWait和volatile一点温习
今天看ConcurrentQueue<T> 源码发现里面居然没有用到lock,我记得ConcurrentDictionary里面是有lock的,lock的是字典里面每一个key,但是ConcurrentQueue<T> 的线程安全确是用SpinWait对象和volatile关键字来实现,于是乎就温习了一下,直接上code
class Test { /* 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。 */ static volatile bool _isCompleted = false; static void UserModeWait() { while (!_isCompleted) { Console.Write("②."); } Console.WriteLine(); Console.WriteLine("③等待完成"); } static void HybridSpinWait() { /* 自旋等待 一个轻量同步类型(结构体),提供对基于自旋的等待的支持。SpinWait只有在多核处理器下才具有使用意义。在单处理器下,自旋转会占据CPU时间,却做不了任何事。 SpinWait并没有设计为让多个任务或线程并发使用。因此,如果多个任务或者线程通过SpinWait的方法进行自旋,那么每一个任务或线程都应该使用自己的SpinWait实例。 */ var w = new SpinWait(); while (!_isCompleted) { // 执行单一自旋。 w.SpinOnce(); /* 判断对SpinWait.SpinOnce() 的下一次调用是否触发上下文切换和内核转换。 由NextSpinWillYield属性代码可知,若SpinWait运行在单核计算机上,它总是进行上下文切换(让出处理器)。 SpinWait不仅仅是一个空循环。它经过了精心实现,可以针对一般情况提供正确的旋转行为以避免内核事件所需的高开销的上下文切换和内核转换; 在旋转时间足够长的情况下自行启动上下文切换,SpinWait甚至还会在多核计算机上产生线程的时间片(Thread.Yield())以防止等待线程阻塞高优先级的线程或垃圾回收器线程。 */ Console.WriteLine("是否触发上下文切换和内核转换: " + w.NextSpinWillYield); } Console.WriteLine("③等待完成"); } public static void RunTest() { var t1 = new Thread(UserModeWait); var t2 = new Thread(HybridSpinWait); Console.WriteLine("①运行用户模式等待"); t1.Start(); //将当前执行RunTest()方法线程挂起指定的时间。 让t1线程执行输出②. Thread.Sleep(1); _isCompleted = true; //将当前线程阻塞指定的时间 Thread.Sleep(TimeSpan.FromSeconds(1)); _isCompleted = false; Console.WriteLine("①运行混合SpinWait构造 等待"); t2.Start(); Thread.Sleep(5); _isCompleted = true; Console.ReadKey(); } }
在网上找了一段java的描述
恐怕比较一下volatile和synchronized的不同是最容易解释清楚的。volatile是变量修饰符,而synchronized则作用于一段代码或方法;看如下三句get代码:
- int i1; int geti1() {return i1;}
- volatile int i2; int geti2() {return i2;}
- int i3; synchronized int geti3() {return i3;}
geti1()得到存储在当前线程中i1的数值。多个线程有多个i1变量拷贝,而且这些i1之间可以互不相同。换句话说,另一个线程可能已经改 变了它线程内的i1值,而这个值可以和当前线程中的i1值不相同。事实上,Java有个思想叫“主”内存区域,这里存放了变量目前的“准确值”。每个线程 可以有它自己的变量拷贝,而这个变量拷贝值可以和“主”内存区域里存放的不同。因此实际上存在一种可能:“主”内存区域里的i1值是1,线程1里的i1值 是2,线程2里的i1值是3——这在线程1和线程2都改变了它们各自的i1值,而且这个改变还没来得及传递给“主”内存区域或其他线程时就会发生。
而geti2()得到的是“主”内存区域的i2数值。用volatile修饰后的变量不允许有不同于“主”内存区域的变量拷贝。换句话说,一个变量经 volatile修饰后在所有线程中必须是同步的;任何线程中改变了它的值,所有其他线程立即获取到了相同的值。理所当然的,volatile修饰的变量 存取时比一般变量消耗的资源要多一点,因为线程有它自己的变量拷贝更为高效。
既然volatile关键字已经实现了线程间数据同步,又要 synchronized干什么呢?呵呵,它们之间有两点不同。首先,synchronized获得并释放监视器——如果两个线程使用了同一个对象锁,监 视器能强制保证代码块同时只被一个线程所执行——这是众所周知的事实。但是,synchronized也同步内存:事实上,synchronized在“ 主”内存区域同步整个线程的内存。因此,执行geti3()方法做了如下几步:
1. 线程请求获得监视this对象的对象锁(假设未被锁,否则线程等待直到锁释放)
2. 线程内存的数据被消除,从“主”内存区域中读入(Java虚拟机能优化此步。。。[后面的不知道怎么表达,汗])
3. 代码块被执行
4. 对于变量的任何改变现在可以安全地写到“主”内存区域中(不过geti3()方法不会改变变量值)
5. 线程释放监视this对象的对象锁
因此volatile只是在线程内存和“主”内存间同步某个变量的值,而synchronized通过锁定和解锁某个监视器同步所有变量的值。显然synchronized要比volatile消耗更多资源。
更通俗的解释:
Volatile 字面的意思时易变的,不稳定的。在C#中也差不多可以这样理解。
编译器在优化代码时,可能会把经常用到的代码存在Cache里面,然后下一次调用就直接读取Cache而不是内存,这样就大大提高了效率。但是问题也随之而来了。
在多线程程序中,如果把一个变量放入Cache后,又有其他线程改变了变量的值,那么本线程是无法知道这个变化的。它可能会直接读Cache里的数据。但是很不幸,Cache里的数据已经过期了,读出来的是不合时宜的脏数据。这时就会出现bug。
用Volatile声明变量可以解决这个问题。用Volatile声明的变量就相当于告诉编译器,我不要把这个变量写Cache,因为这个变量是可能发生改变的。
C#SpinWait和volatile一点温习