非阻塞同步 - 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#学习笔记之线程 - 高级主题:非阻塞同步的更多相关文章

  1. python学习笔记之四-多进程&多线程&异步非阻塞

    ProcessPoolExecutor对multiprocessing进行了高级抽象,暴露出简单的统一接口. 异步非阻塞 爬虫 对于异步IO请求的本质则是[非阻塞Socket]+[IO多路复用]: & ...

  2. 操作系统学习笔记----进程/线程模型----Coursera课程笔记

    操作系统学习笔记----进程/线程模型----Coursera课程笔记 进程/线程模型 0. 概述 0.1 进程模型 多道程序设计 进程的概念.进程控制块 进程状态及转换.进程队列 进程控制----进 ...

  3. JUC源码学习笔记5——线程池,FutureTask,Executor框架源码解析

    JUC源码学习笔记5--线程池,FutureTask,Executor框架源码解析 源码基于JDK8 参考了美团技术博客 https://tech.meituan.com/2020/04/02/jav ...

  4. java学习笔记15--多线程编程基础2

    本文地址:http://www.cnblogs.com/archimedes/p/java-study-note15.html,转载请注明源地址. 线程的生命周期 1.线程的生命周期 线程从产生到消亡 ...

  5. 简单测试Java线程安全中阻塞同步与非阻塞同步性能

    摘抄自周志明老师的<深入理解Java虚拟机:JVM高级特性与最佳实践>13.2.2 线程安全的实现方法 1.名词解释 同步是指锁哥线程并发访问共享数据时,保证共享数据同一时刻只被一个线程访 ...

  6. 《java并发编程实战》读书笔记12--原子变量,非阻塞算法,CAS

    第15章 原子变量与非阻塞同步机制 近年来,在并发算法领域的大多数研究都侧重于非阻塞算法,这种算法用底层的原子机器指令(例如比较并交换指令)代替锁老确保数据在并发访问中的一致性. 15.1 锁的劣势 ...

  7. 进程理论 阻塞非阻塞 同步异步 I/O操作

    1.什么是进程 进程指的是一个正在运行的程序,进程是用来描述程序执行过程的虚拟概念 进程的概念起源于操作系统,进程是操作系统最核心的概念,操作系统其它所有的概念都是围绕进程来的 2.操作系统 操作系统 ...

  8. 深入理解非阻塞同步IO和非阻塞异步IO

    这两篇文章分析了Linux下的5种IO模型 http://blog.csdn.net/historyasamirror/article/details/5778378 http://blog.csdn ...

  9. 从同步原语看非阻塞同步以及Java中的应用

    非阻塞同步:基于冲突检测的乐观并发策略,通俗讲就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了,如果争用数据有冲突那就采用其他的补偿措施(最常见的就是不断重试直到成功),这种乐观的并发策略 ...

随机推荐

  1. PostgreSQL的 initdb 源代码分析之十四

    继续分析: /* * Make the per-database PG_VERSION for template1 only after init'ing it */ write_version_fi ...

  2. 分析代码的利器 - ctags

    比方我们在分析代码的时候,须要看某一个方法或类的定义,我们须要临时跳转过去看一下,然后还能非常方便的回来.这时候ctags就派上用场了. 比方你有一个src目录,先用ctags对其生成索引: ctag ...

  3. Educational Codeforces Round 2 C. Make Palindrome 贪心

    C. Make Palindrome Time Limit: 20 Sec Memory Limit: 256 MB 题目连接 http://codeforces.com/contest/600/pr ...

  4. ABAP FIELD-SYMBOLS 有大作用- 将没有可改参数的增强出口变得也能改主程序的值了

    看下图代码: report  z_xul_test2 中 定义了 全局变量 G_DATA1 , 分别调用了 z_xul_tes1 中的 form  和 function zbapi_test , 这两 ...

  5. c++中的强制转换static_cast、dynamic_cast、reinterpret_cast的不同用法儿

    c++中的强制转换static_cast.dynamic_cast.reinterpret_cast的不同用法儿   虽然const_cast是用来去除变量的const限定,但是static_cast ...

  6. string 对象及其操作

    标准库类型string 标准库类型string表示可变长的字符序列,使用string类型必须首先包含string头文件.作为标准库的一部分,string定义在命名空间std中.接下来的示例都假定了已包 ...

  7. [带你飞]一小时带你学会Python

    1.面向的读者: 具有Javascript经验的程序猿. 2 快速入门2.1 Hello world 安装完Python之后,打开IDLE(Python GUI) , 该程序是Python语言解释器, ...

  8. linq小知识总结

    1linq的左连接查询 var boundList = from x in text.S_Outbound join y in text.S_Outbound_Per on x.Shipment_ID ...

  9. apache常见错误汇总

    <>问题: Access forbidden! You don't have permission to access the requested directory. There is ...

  10. Ng-include 例子

    <body> <div ng-app="myApp"> <div ng-controller="firstController"& ...