C#学习笔记之线程 - 高级主题:非阻塞同步
非阻塞同步 - Nonblock Synchronization
前面提到,即使在简单的赋值和增加一个字段的情况下也需要处理同步。尽管,使用锁可以完成这个功能,但是锁必定会阻塞线程,需要线程切换,在高并发的场景中,这使非常关键的。.NET框架的非阻塞同步能够执行简单的操作而不需要阻塞,暂停或等待。
编写非阻塞或无锁的多线程代码是一种技巧。内存屏障很容易出错(volatile关键字更容易出错)。仔细想一想,在你不使用锁之前,你是否真的需要这些性能。毕竟,获取和释放一个不竞争的锁还不需20ns。
非阻塞方法也可以跨进程。在读写进程间共享内存时可能有用。
内存屏障和Volatility
想一想下面的代码:
class Foo
{
int _answer;
bool _complete;
void A()
{
_answer = ;
_complete = true;
}
void B()
{
if (_complete) Console.WriteLine (_answer);
}
}
如果A和B同时运行在不同的线程上,B是否有可能输入0?答案是Yes--因为以下2个原因:
- 编译器,CLR或CPU可能为了改善效率重新排序了程序指令。
- 编译器,CLR或CPU可能引入了cache来优化变量的赋值,但是其它线程不能立即看到。
C#和CLR非常小心地确保这样的优化不会打断普通的单线程代码--或者正确使用锁的多线程代码。这些场景之外,你必须显式地通过创建内存屏障来击败这些优化,确保限制指令的重新排序和读写缓存的影响。
完全内存屏障 full memory barrier (full fence)
最简单的内存屏障是完全内存屏障,阻止任何对指令的排序和缓存。调用Thread.MemoryBarrier产生一个完全内存屏障,我们可以通过full fence来解决这个问题:
class Foo
{
int _answer;
bool _complete;
void A()
{
_answer = ;
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和4阻止写“0”。Barrier2和3保证:如果B在A之后运行,_complete肯定是true。一个full fence只需10ns。
下面隐式地产生了full fences:
- C#的lock语句(Montor.Enter/Montor.Exit)
- Interlocked类的所有方法
- 使用线程池的异步回调--包含异步委托,APM回调和Task
- Set和等待Signal
- 任何依赖于Signal的东西,如在Task上开始或等待的事情。下面的代码是线程安全的:
int x=;
Task t = Task.Factory.StartNew(()=>x++);
t.Wait();
Console.WriteLine(x);
不必为每一个读写都使用full fence。如果你3个answer字段,我们也只需4个fences:
class Foo
{
int _answer1, _answer2, _answer3;
bool _complete;
void A()
{
_answer1 = ; _answer2 = ; _answer3 = ;
Thread.MemoryBarrier();
_complete = true;
Thread.MemoryBarrier();
}
void B()
{
Thread.MemoryBarrier();
if (_complete)
{
Thread.MemoryBarrier();
Console.WriteLine (_answer1 + _answer2 + _answer3);
}
}
}
一个好的方法是在读写每一个共享字段前后都加上内存屏障,跳过你不需要的。如果你不确定,随他去。更好的办法是:使用锁。
确实需要Lock和内存屏障吗?
与没有加锁或内存屏障的共享写字段工作是自找麻烦。这里有大量的误用--包括MSND对于MemoryBarrier的文档,它说仅在多盒处理器,如有多个Itanium处理器的系统中才要求MemoryBarrier。我们演示的例子揭示了内存屏障在Interl core-2处理器上的重要性。你需要优化它并不能在debug模式下(在Visual Studio中选择Release,并且以非debug方式启动)。
static void Main()
{
bool complete = false;
var t = new Thread (() =>
{
bool toggle = false;
while (!complete) toggle = !toggle;
});
t.Start();
Thread.Sleep ();
complete = true;
t.Join(); // Blocks indefinitely
}
这个程序不会终止,因为complete变量被缓存在CPU的寄存器中。在while循环中插入一个MemoryBarrier(或者围绕读complete加锁)可以解决这个问题。
关键字volatile
另外一个解决这个问题的方法是对complete使用volatile关键字。volatile bool complete;
关键字volatile指示编译器在每次读这个字段时产生一个获取屏障,并在每次写字段时释放屏障。一个获取屏障在屏障之前阻止其它读写被移动;释放屏障阻止在屏障之后其它读写被移动。这些半屏障比full fence更快。
到目前为止,Intel的X86和X64处理器总是使用获取屏障来读及写后释放屏障--不管你是否使用volatile关键字--所以这个关键字对于正在使用这些处理器人没有任何影响。但是,volatile在编译器和CLR上执行优化有影响,64位的AMD和Itanium处理器也有影响。这意味着你不会更轻松,因为你的程序运行在不同的处理器上。
如果你使用volatile,那么说明你渴望你的程序更加健康。
对字段使用volatile的影响概括如下:
First instruction | Second instruction | Can they be swapped? |
Read | Read | No |
Read | Write | No |
Write | Write | No (The CLR ensures that write-write operations are never swapped, even without the volatile keyword |
Write | Read | Yes |
可以看出volatile并不阻止写紧接着读可以被交换,这就像脑筋急转弯。Joe Duffy用下面的例子很好的演示了这个问题:如果Test1和Test2同时运行在不同的线程上,a和b结束时同时为0这是可能的(不管你是否对x和y使用volatile)。
class IfYouThinkYouUnderstandVolatile
{
volatile int x, y;
void Test1() // Executed on one thread
{
x = ; // Volatile write (release-fence)
int a = y; // Volatile read (acquire-fence)
...
}
void Test2() // Executed on another thread
{
y = ; // Volatile write (release-fence)
int b = x; // Volatile read (acquire-fence)
...
}
}
MSDN上说使用volatile关键字可以确保任何时候它的值是最新的。这是不正确的,因为我们已经看到写紧接着读是可能重新排序的。
这强烈说明应该避免使用volatile:即使你理解这个例子的细节,其它开发者呢?在每个赋值语句中使用完成内存屏障或锁可以解决这个问题。
volatile并不支持通过引用传递给参数或局部变量:这些情况应该使用volatileRead和VolatileWrite函数。
VolatileRead和VolatileWrite
这2个方法是Thread的静态方法来读写一个变量,被volatile关键字强迫保证。它们的实现也相对低效,实际上它们是通过full fence来实现的。下面是对integer类型完整实现:
public static void VolatileWrite(ref in address, int value)
{
MemoryBarrier();address=value;
}
public static void VolatileRead(ref int address)
{
int num =address; MemoryBarrier();return num;
}
从中可以看出,如果你使用VolatileWrite紧接着调用VolatileRead,那么在两者之间没有屏障:前面的问题又出现了。
内存屏障和锁 - Memory barrier & lock
前面提到,Monitor.Enter和Monitor.Exit都产生了完全屏障。所以如果我们忽略锁的排斥保证,那么可以这么认为:
lock(someField){...}等价于Thread.MemoryBarrier();{...}Thread.MemoryBarrier();
Interlocked
在一个不使用锁的代码中只用内存屏障是不够的。在64位的字段上的自增或自减要求使用Interlocked类。Interlocked类也提供了Exchange和CompareExchange方法,后者不用锁,能读-修改-写操作,而不需要额外的代码。
如果一个语句在处理器上作为一个指令执行那么它就是原子的。严格的原子性排除了任何被抢占的可能性。一个32位字段的读写或者小于总是原子操作的。64位的字段在64位的运行时环境中也是原子的,超过一个读写操作的语句不是原子的。
class Atomicity
{
static int _x, _y;
static long _z;
static void Test()
{
long myLocal;
_x = 3; // Atomic
_z = 3; // Nonatomic on 32-bit environs (_z is 64 bits)
myLocal = _z; // Nonatomic on 32-bit environs (_z is 64 bits)
_y += _x; // Nonatomic (read AND write operation)
_x++; // Nonatomic (read AND write operation)
}
}
读写一个64位的字段在32位环境中是非原子的,因为这要求2条指令:每个32位内存位置1条。所以,如果当Y线程正在更新它,而线程X正在读取,那么X线程可能读到不正确的值。
编译器实现一个二元运算x++,是通过读取变量,处理它并写回来实现的。
下面的例子:
class ThreadUnsafe
{
static int _x=1000;
static void Go(){for(int i=0;i<100;i++)_x--;}
}
把内存屏障放在一边,你可能预期如果10个线程同时运行Go,_x可能最后是0。然而,这是不保证的,因为这里有一个竞争条件:其它线程可能在当前线程找回x的当前值,递增它并写回这个过程中抢占(导致它的值不是最新的)。
当然,你可以用lock来封装这些非原子代码来解决这个问题。Interlocked为这些简单的操作提供了一个更简单,更快的解决方案。
Interlocked的数学运算仅限于Increment,Decrement和Add。如果你想要乘法或除法运算,你可以在不使用锁的代码中使用CompareExchange来完成(通常与自旋等待连用)。
操作系统和虚拟机知道Interlocked需要原子性操作。
Interlocked这类函数大概需要10ns的时间--大概是无竞争lock的一半时间。而且,它们从来没有由于阻塞而切换上下文的花费。在一个循环内部使用Interlocked可能比围绕循环加锁更加低效。
C#学习笔记之线程 - 高级主题:非阻塞同步的更多相关文章
- python学习笔记之四-多进程&多线程&异步非阻塞
ProcessPoolExecutor对multiprocessing进行了高级抽象,暴露出简单的统一接口. 异步非阻塞 爬虫 对于异步IO请求的本质则是[非阻塞Socket]+[IO多路复用]: & ...
- 操作系统学习笔记----进程/线程模型----Coursera课程笔记
操作系统学习笔记----进程/线程模型----Coursera课程笔记 进程/线程模型 0. 概述 0.1 进程模型 多道程序设计 进程的概念.进程控制块 进程状态及转换.进程队列 进程控制----进 ...
- JUC源码学习笔记5——线程池,FutureTask,Executor框架源码解析
JUC源码学习笔记5--线程池,FutureTask,Executor框架源码解析 源码基于JDK8 参考了美团技术博客 https://tech.meituan.com/2020/04/02/jav ...
- java学习笔记15--多线程编程基础2
本文地址:http://www.cnblogs.com/archimedes/p/java-study-note15.html,转载请注明源地址. 线程的生命周期 1.线程的生命周期 线程从产生到消亡 ...
- 简单测试Java线程安全中阻塞同步与非阻塞同步性能
摘抄自周志明老师的<深入理解Java虚拟机:JVM高级特性与最佳实践>13.2.2 线程安全的实现方法 1.名词解释 同步是指锁哥线程并发访问共享数据时,保证共享数据同一时刻只被一个线程访 ...
- 《java并发编程实战》读书笔记12--原子变量,非阻塞算法,CAS
第15章 原子变量与非阻塞同步机制 近年来,在并发算法领域的大多数研究都侧重于非阻塞算法,这种算法用底层的原子机器指令(例如比较并交换指令)代替锁老确保数据在并发访问中的一致性. 15.1 锁的劣势 ...
- 进程理论 阻塞非阻塞 同步异步 I/O操作
1.什么是进程 进程指的是一个正在运行的程序,进程是用来描述程序执行过程的虚拟概念 进程的概念起源于操作系统,进程是操作系统最核心的概念,操作系统其它所有的概念都是围绕进程来的 2.操作系统 操作系统 ...
- 深入理解非阻塞同步IO和非阻塞异步IO
这两篇文章分析了Linux下的5种IO模型 http://blog.csdn.net/historyasamirror/article/details/5778378 http://blog.csdn ...
- 从同步原语看非阻塞同步以及Java中的应用
非阻塞同步:基于冲突检测的乐观并发策略,通俗讲就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了,如果争用数据有冲突那就采用其他的补偿措施(最常见的就是不断重试直到成功),这种乐观的并发策略 ...
随机推荐
- Unix: How to Install BerkeleyDB From Source
http://www.masaokitamura.com/2010/07/23/unix-how-to-install-berkeleydb-from-source/ This documentati ...
- Android下结束进程的方法
转自:http://www.cnblogs.com/crazypebble/archive/2011/04/05/2006213.html 最近在做一个类似与任务管理器的东西,里面有个功能,可以通过这 ...
- 判断sqlserver临时表等临时资源是否存在
if exists(select * from tempdb..sysobjects where id=object_id('tempdb..#TEMP')) drop table #TEMP
- 很值得学习的java 画图板源码
很值得学习的java 画图板源码下载地址:http://download.csdn.net/source/2371150 package minidrawpad; import java.awt.*; ...
- 如何将一个 ASP.NET MVC 4 和 Web API 项目升级到 ASP.NET MVC 5 和 Web API 2
----转自微软官网www.asp.net/mvc/ ASP.NET MVC 5 和 Web API 2 带来的新功能,包括属性路由. 身份验证筛选器,以及更多的主机.请参阅http://www.as ...
- ajax生成html双引号问题
//动态创建列表 function createLists(result){ var len=result.length,i; for(i=0;i<len;i++){ $myLi = $(&qu ...
- 前端必会html知识整理
1.浏览器内核: 1.ie:trident(三叉戟)内核 2.firefox:gecko(壁虎)内核 3.safari:webkit(浏览器核心)内核 ...
- jQuery 插件写法2
转载:http://www.xuanfengge.com/jquery-plug-in-written-summary-and-summary-of-writing-object-oriented-m ...
- 一个JS版本的MD5
var hexcase = 0; function hex_md5(a) { if (a == "") return a; return rstr2hex(rstr_md5(str ...
- 对x264_macroblock_cache_load的理解
X264版本: 2004/06/03 函数作用: 将编码该宏块所需的信息加载到mb.pic.mb.cache两个结构体中,记录相邻宏块的存在性. 函数过程: 初始化坐标信息,这些坐标信息将在下面用作下 ...