上一节介绍了使用信号量进行同步,本节主要介绍一些非阻塞同步的方法。本节主要介绍MemoryBarrier,volatile,Interlocked。

MemoryBarriers

本文简单的介绍一下这两个概念,假设下面的代码:

using System;
class Foo
{
int _answer;
bool _complete; void A()
{
_answer = 123;
_complete = true;
} void B()
{
if (_complete) Console.WriteLine(_answer);
}
}

如果方法A和方法B同时在两个不同线程中运行,控制台可能输出0吗?答案是可能的,有以下两个原因:

  • 编译器,CLR或者CPU可能会更改指令的顺序来提高性能
  • 编译器,CLR或者CPU可能会通过缓存来优化变量,这种情况下对其他线程是不可见的。

最简单的方式就是通过MemoryBarrier来保护变量,来防止任何形式的更改指令顺序或者缓存。调用Thread.MemoryBarrier会生成一个内存栅栏,我们可以通过以下的方式解决上面的问题:

using System;
using System.Threading;
class Foo
{
int _answer;
bool _complete; void A()
{
_answer = 123;
Thread.MemoryBarrier(); // Barrier 1
_complete = true;
Thread.MemoryBarrier(); // Barrier 2
} void B()
{
Thread.MemoryBarrier(); // Barrier 3
if (_complete)
{
Thread.MemoryBarrier(); // Barrier 4
Console.WriteLine(_answer);
}
}
}

上面的例子中,barrier1和barrier3用来保证指令顺序不会改变,barrier2和barrier4用来保证值变化不被缓存。一个好的处理方案就是我们在需要保护的变量前后分别加上MemoryBarrier。

在c#中,下面的操作都会生成MemoryBarrier:

  • Lock语句(Monitor.Enter,Monitor.Exit)
  • 所有Interlocked类的方法
  • 线程池的回调方法
  • Set或者Wait信号
  • 所有依赖于信号灯实现的方法,如starting或waiting 一个Task

因为上面这些行为,这段代码实际上是线程安全的:

        int x = 0;
Task t = Task.Factory.StartNew(() => x++);
t.Wait();
Console.WriteLine(x); // 1

在你自己的程序中,你可能重现不出来上面例子所说的情况。事实上,从msdn上对MomoryBarrier的解释来看,只有对顺序保护比较弱的多核系统才需要用到MomoryBarrier。但是有一点需要注意:多线程去修改变量并且不使用任何形似的锁或者内存栅栏是会带来一定的麻烦的。

下面一个例子能够很好的说明上面的观点(在你的VisualStudio中,选择Release模式,并且Start Without Debugging重现这个问题):

        bool complete = false;
var t = new Thread(() =>
{
bool toggle = false;
while (!complete) toggle = !toggle;
});
t.Start();
Thread.Sleep(1000);
complete = true;
t.Join(); // Blocks indefinitely

这个程序永远不会结束,因为complete变量被缓存在了CPU寄存器中。在while循环中加入Thread.MemoryBarrier可以解决这个问题。

volatile关键字

另外一种更高级的方式来解决上面的问题,那就是考虑使用volatile关键字。Volatile关键字告诉编译器在每一次读操作时生成一个fence,来实现保护保护变量的目的。具体说明可以参见msdn的介绍

VolatileRead和VolatileWrite

Volatile关键字只能加到类变量中。本地变量不能被声明成volatile。这种情况你可以考虑使用System.Threading.Volatile.Read方法。我们看一下System.Threading.Volatile源码如何实现这两个方法的:

    public static bool Read(ref bool location)
{
bool flag = location;
Thread.MemoryBarrier();
return flag;
}
public static void Write(ref bool location, bool value)
{
Thread.MemoryBarrier();
location = value;
}

  

一目了然,通过MemoryBarrier来实现的,但是他只在读操作的后面和写操作的前面加了MemoryBarrier,那么你应该考虑,如果你先使用Volatile.Write再使用Volatile.Read是不是可能有问题呢?

c#中ConcurrentDictionary中使用了Volatile类来保护变量,有兴趣的读者可以看看c#的开发者是如何使用这个方法来保护变量的。

Interlocked

使用MemoryBarrier并不总是一个好的解决方案,尤其在不需要锁的情况下。Interlocked方法提供了一些常用的原子操作来避免前面文章提到的一系列的问题。如使用Interlocked.Increment来替代++,Interlocked.Decrement来替代--。Msdn的文档中详细的介绍了相关的用法和原理。C#中的源码里也经常能看见Interlocked相关的使用。

本文介绍了一些除了锁和信号量之外的一些同步方式,欢迎批评与指正。

细说.NET中的多线程 (六 使用MemoryBarrier,Volatile进行同步)的更多相关文章

  1. 细说.NET 中的多线程 (一 概念)

    为什么使用多线程 使用户界面能够随时相应用户输入 当某个应用程序在进行大量运算时候,为了保证应用程序能够随时相应客户的输入,这个时候我们往往需要让大量运算和相应用户输入这两个行为在不同的线程中进行. ...

  2. 细说.NET中的多线程 (二 线程池)

    上一章我们了解到,由于线程的创建,销毁都是需要耗费大量资源和时间的,开发者应该非常节约的使用线程资源.最好的办法是使用线程池,线程池能够避免当前进行中大量的线程导致操作系统不停的进行线程切换,当线程数 ...

  3. 细说.NET中的多线程 (五 使用信号量进行同步)

    上一节主要介绍了使用锁进行同步,本节主要介绍使用信号量进行同步 使用EventWaitHandle信号量进行同步 EventWaitHandle主要用于实现信号灯机制.信号灯主要用于通知等待的线程.主 ...

  4. 细说.NET中的多线程 (三 使用Task)

    上一节我们介绍了线程池相关的概念以及用法.我们可以发现ThreadPool. QueueUserWorkItem是一种起了线程之后就不管了的做法.但是实际应用过程,我们往往会有更多的需求,比如如果更简 ...

  5. 细说.NET中的多线程 (四 使用锁进行同步)

    通过锁来实现同步 排它锁主要用来保证,在一段时间内,只有一个线程可以访问某一段代码.两种主要类型的排它锁是lock和Mutex.Lock和Mutex相比构造起来更方便,运行的也更快.但是Mutex可以 ...

  6. 38 多线程(十)——volatile 数据同步

    在多线程并发的情况下,同一个变量被多个线程调用,那修改的数据就不会每分每秒保持一致.例如,对于某个变量a,线程1对它进行一套操作,线程2又对它进行另一套操作,但如果cpu太忙了,太忙了,假设cpu都用 ...

  7. NET 中的多线程

    NET 中的多线程 为什么使用多线程 使用户界面能够随时相应用户输入 当某个应用程序在进行大量运算时候,为了保证应用程序能够随时相应客户的输入,这个时候我们往往需要让大量运算和相应用户输入这两个行为在 ...

  8. C#中的多线程 - 并行编程 z

    原文:http://www.albahari.com/threading/part5.aspx 专题:C#中的多线程 1并行编程Permalink 在这一部分,我们讨论 Framework 4.0 加 ...

  9. C#中的多线程 - 同步基础

    原文:http://www.albahari.com/threading/part2.aspx 文章来源:http://blog.gkarch.com/threading/part2.html 1同步 ...

随机推荐

  1. 跟我学-Java底层技术系列文章

    对于工作中经常用到的东西,还是多看看实现原理,这样用着才能放心. 源码思想学习计划: 1.java基础库  HashCode深入理解 java线程框架窥探 2.集合类     java枚举类使用 递归 ...

  2. 【Mail】telnet收发邮件过程

    telnet发送邮件 关于电子邮件的基础知识请参照:[Mail]邮件的基础知识和原理 smtp协议是一个简单的邮件传输协议,利用它我们可以将邮件发送给别人,这里将通过telnet这个程序利用smtp协 ...

  3. SQL Server Update 语句使用Nolock 语法

    Update talblename set Column='XX' from Table TableName with(nolock) where XXX

  4. [C#基础实例]指定地址解析图片并下载

    需求:查找页面图片并下载至本地: 实现: 首先:读取通过网络html内容,并用正则表达式查找图片地下. 其次:使用WebRequest.Create创建图片请求. 最后:把获取图片网络流数据通过Fil ...

  5. 用js效果做的简单焦点图

    /*js代码*/ <script src="js/js/myfocus-2.0.1.min.js" type="text/javascript">& ...

  6. python+eclipse环境搭建

    一.Eclipse 的安装 Eclipse的安装是很容易的.Eclipse是基于java的一个应用程序,因此需要一个java的运行环境(JRE)才行.(我这里主要介绍windows下的安装) JRE的 ...

  7. JavaScript-永远点不到的小窗口

    <!doctype html> <html> <head> <meta charset="UTF-8"> <title> ...

  8. zmq中zmq_poll()函数介绍

    功能: 查看指定的多个socket上哪些socket发生了指定的事件, 事件有: ZMQ_POLLIN: 有消息到来 ZMQ_POLLOUT: 当前无阻塞可以发送消息 ZMQ_POLLERR: 只对标 ...

  9. loadRunner 负载机连接错误分析

    错误信息: Error: Process "lr_bridge.exe" was not created on remote host "192.168.86.17&qu ...

  10. toad的基本操作

    1.把鼠标停在sql所在行,然后ctrl+Enter直接执行当前sql. 2.解决Toad对中文显示乱码问题(如果数据库所在主机的NLS_LANG是AMERICAN_AMERICA.WE8ISO885 ...