-
C#教程之深入多线程之:内存栅栏与volatile关键字的
以前我们说过在一些简单的例子中,比如为一个字段赋值或递增该字段,我们需要对线程进行同步,
虽然lock可以满足我们的需要,但是一个竞争锁一定会导致阻塞,然后忍受线程上下文切换和调度的开销,在一些高并发和性能比较关键的地方,这些是不能忍受的。
.net framework 提供了非阻塞同步构造,为一些简单的操作提高了性能,它甚至都没有阻塞,暂停,和等待线程。
Memory Barriers and Volatility (内存栅栏和易失字段 )
考虑下下面的代码:
int _answer;
bool _complete;
void A()
{
_answer = 123;
_complete = true;
}
void B()
{
if (_complete)
Console.WriteLine(_answer);
}
如果方法A和B都在不同的线程下并发的执行,方法B可能输出 “0” 吗?
回答是“yes”,基于以下原因:
编译器,clr 或 cpu 可能会为了性能而重新为程序的指令进行排序,例如可能会将方法A中的两句代码的顺序进行调整。
编译器,clr 或 cpu 可能会为变量的赋值采用缓存策略,这样这些变量就不会立即对其他变量可见了,例如方法A中的变量赋值,不会立即刷新到内存中,变量B看到的变量并不是最新的值。
C# 和运行时非常小心的保证这些优化策略不会影响正常的单线程的代码和在多线程环境下加锁的代码。
除此之外,你必须显示的通过创建内存屏障(Memory fences) 来限制指令重新排序和读写缓存对程序造成的影响。
Full fences:
最简单的完全栅栏的方法莫过于使用Thread.MemoryBarrier方法了。
以下是msdn的解释:
Thread.MemoryBarrier: 按如下方式同步内存访问:执行当前线程的处理器在对指令重新排序时,不能采用先执行 MemoryBarrier 调用之后的内存访问,再执行 MemoryBarrier 调用之前的内存访问的方式。
按照我个人的理解:就是写完数据之后,调用MemoryBarrier,数据就会立即刷新,另外在读取数据之前调用MemoryBarrier可以确保读取的数据是最新的,并且处理器对MemoryBarrier的优化小心处理。
int _answer;
bool _complete;
void A()
{
_answer = 123;
Thread.MemoryBarrier(); //在写完之后,创建内存栅栏
_complete = true;
Thread.MemoryBarrier();//在写完之后,创建内存栅栏
}
void B()
{
Thread.MemoryBarrier();//在读取之前,创建内存栅栏
if (_complete)
{
Thread.MemoryBarrier();//在读取之前,创建内存栅栏
Console.WriteLine(_answer);
}
}
一个完全的栅栏在现代桌面应用程序中,大于需要花费10纳秒。
下面的一些构造都隐式的生成完全栅栏。
C# Lock 语句(Monitor.Enter / Monitor.Exit)
在Interlocked类的所有方法。
使用线程池的异步回调,包括异步的委托,APM 回调,和 Task continuations.
在一个信号构造中的发送(Settings)和等待(waiting)
你不需要对每一个变量的读写都使用完全栅栏,假设你有三个answer 字段,我们仍然可以使用4个栅栏。例如:
int _answer1, _answer2, _answer3;
bool _complete;
void A()
{
_answer1 = 1; _answer2 = 2; _answer3 = 3;
Thread.MemoryBarrier(); //在写完之后,创建内存栅栏
_complete = true;
Thread.MemoryBarrier(); //在写完之后,创建内存栅栏
}
void B()
{
Thread.MemoryBarrier(); //在读取之前,创建内存栅栏
if (_complete)
{
Thread.MemoryBarrier(); //在读取之前,创建内存栅栏
Console.WriteLine(_answer1 + _answer2 + _answer3);
}
}
我们真的需要lock 和内存栅栏吗?
在一个共享可写的字段上不使用lock 或者栅栏 就是在自找麻烦,在msdn上有很多关于这方面的主题。
考虑下下面的代码:
public static void Main()
{
bool complete = false;
var t = new Thread(() =>
{
bool toggle = false;
while (!complete) toggle = !toggle;
});
t.Start();
Thread.Sleep(1000);
complete = true;
t.Join();
}
如果你在Visual Studio中选择发布模式,生成该应用程序,那么如果你直接运行应用程序,程序都不会中止。
因为CPU 寄存器把 complete 变量的值给缓存了。在寄存器中,complete永远都是false。
通过在while循环中插入Thread.MemoryBarrier,或者是在读取complete的时候加锁 都可以解决这个问题。
volatile 关键字
为_complete字段加上volatile关键字也可以解决这个问题。
volatile bool _complete.
Volatile关键字会指导编译器自动的为读写字段加屏障.以下是msdn的解释:
volatile 关键字指示一个字段可以由多个同时执行的线程修改。声明为 volatile 的字段不受编译器优化(假定由单个线程访问)的限制。这样可以确保该字段在任何时间呈现的都是最新的值。
使用volatile字段可以被总结成下表:
注意到应用volatile关键字,并不能保证写后面跟读的操作不被交换,这有可能会造成莫名其妙的问题。例如:
volatile int x, y;
void Test1()
{
x = 1; //Volatile write
int a = y; //Volatile Read
}
void Test2()
{
y = 1; //Volatile write
int b = x; //Volatile Read
}
如果Test1和Test2在不同的线程中并发执行,有可能a 和b 字段的值都是0,(尽管在x和y上应用了volatile 关键字)
这是一个避免使用volatile关键字的好例子,甚至假设你彻底的明白了这段代码,是不是其他在你的代码上工作的人也全部明白呢?。
在Test1 和Test2方法中使用完全栅栏或者是lock都可以解决这个问题,
还有一个不使用volatile关键字的原因是性能问题,因为每次读写都创建了内存栅栏,例如
volatile m_amount
m_amount = m_amount + m_amount.
Volatile 关键字不支持引用传递的参数,和局部变量。在这样的场景下,你必须使用
VolatileRead和VolatileWrite方法。例如
volatile int m_amount;
Boolean success =int32.TryParse(“123”,out m_amount);
//生成如下警告信息:
//cs0420:对volatile字段的引用不被视为volatile.
VolatileRead 和VolatileWrite
从技术上讲,Thread类的静态方法VolatileRead和VolatileWrite在读取一个 变量上和volatile 关键字的作用一致。
他们的实现是一样是低效率的,尽管事实上他们都创建了内存栅栏。下面是他们在integer类型上的实现。
public static void VolatileWrite(ref int address, int value)
{
Thread.MemoryBarrier(); address = value;
}
public static int VolatileRead(ref int address)
{
int num = address; Thread.MemoryBarrier(); return num;
}
你可以看到如果你在调用VolatileWrite之后调用VolatileRead,在中间没有栅栏会被创建,这同样会导致我们上面讲到写之后再读顺序可能变换的问题。
虽然lock可以满足我们的需要,但是一个竞争锁一定会导致阻塞,然后忍受线程上下文切换和调度的开销,在一些高并发和性能比较关键的地方,这些是不能忍受的。
.net framework 提供了非阻塞同步构造,为一些简单的操作提高了性能,它甚至都没有阻塞,暂停,和等待线程。
Memory Barriers and Volatility (内存栅栏和易失字段 )
考虑下下面的代码:
复制代码 代码如下:
int _answer;
bool _complete;
void A()
{
_answer = 123;
_complete = true;
}
void B()
{
if (_complete)
Console.WriteLine(_answer);
}
如果方法A和B都在不同的线程下并发的执行,方法B可能输出 “0” 吗?
回答是“yes”,基于以下原因:
编译器,clr 或 cpu 可能会为了性能而重新为程序的指令进行排序,例如可能会将方法A中的两句代码的顺序进行调整。
编译器,clr 或 cpu 可能会为变量的赋值采用缓存策略,这样这些变量就不会立即对其他变量可见了,例如方法A中的变量赋值,不会立即刷新到内存中,变量B看到的变量并不是最新的值。
C# 和运行时非常小心的保证这些优化策略不会影响正常的单线程的代码和在多线程环境下加锁的代码。
除此之外,你必须显示的通过创建内存屏障(Memory fences) 来限制指令重新排序和读写缓存对程序造成的影响。
Full fences:
最简单的完全栅栏的方法莫过于使用Thread.MemoryBarrier方法了。
以下是msdn的解释:
Thread.MemoryBarrier: 按如下方式同步内存访问:执行当前线程的处理器在对指令重新排序时,不能采用先执行 MemoryBarrier 调用之后的内存访问,再执行 MemoryBarrier 调用之前的内存访问的方式。
按照我个人的理解:就是写完数据之后,调用MemoryBarrier,数据就会立即刷新,另外在读取数据之前调用MemoryBarrier可以确保读取的数据是最新的,并且处理器对MemoryBarrier的优化小心处理。
复制代码 代码如下:
int _answer;
bool _complete;
void A()
{
_answer = 123;
Thread.MemoryBarrier(); //在写完之后,创建内存栅栏
_complete = true;
Thread.MemoryBarrier();//在写完之后,创建内存栅栏
}
void B()
{
Thread.MemoryBarrier();//在读取之前,创建内存栅栏
if (_complete)
{
Thread.MemoryBarrier();//在读取之前,创建内存栅栏
Console.WriteLine(_answer);
}
}
一个完全的栅栏在现代桌面应用程序中,大于需要花费10纳秒。
下面的一些构造都隐式的生成完全栅栏。
C# Lock 语句(Monitor.Enter / Monitor.Exit)
在Interlocked类的所有方法。
使用线程池的异步回调,包括异步的委托,APM 回调,和 Task continuations.
在一个信号构造中的发送(Settings)和等待(waiting)
你不需要对每一个变量的读写都使用完全栅栏,假设你有三个answer 字段,我们仍然可以使用4个栅栏。例如:
复制代码 代码如下:
int _answer1, _answer2, _answer3;
bool _complete;
void A()
{
_answer1 = 1; _answer2 = 2; _answer3 = 3;
Thread.MemoryBarrier(); //在写完之后,创建内存栅栏
_complete = true;
Thread.MemoryBarrier(); //在写完之后,创建内存栅栏
}
void B()
{
Thread.MemoryBarrier(); //在读取之前,创建内存栅栏
if (_complete)
{
Thread.MemoryBarrier(); //在读取之前,创建内存栅栏
Console.WriteLine(_answer1 + _answer2 + _answer3);
}
}
我们真的需要lock 和内存栅栏吗?
在一个共享可写的字段上不使用lock 或者栅栏 就是在自找麻烦,在msdn上有很多关于这方面的主题。
考虑下下面的代码:
复制代码 代码如下:
public static void Main()
{
bool complete = false;
var t = new Thread(() =>
{
bool toggle = false;
while (!complete) toggle = !toggle;
});
t.Start();
Thread.Sleep(1000);
complete = true;
t.Join();
}
如果你在Visual Studio中选择发布模式,生成该应用程序,那么如果你直接运行应用程序,程序都不会中止。
因为CPU 寄存器把 complete 变量的值给缓存了。在寄存器中,complete永远都是false。
通过在while循环中插入Thread.MemoryBarrier,或者是在读取complete的时候加锁 都可以解决这个问题。
volatile 关键字
为_complete字段加上volatile关键字也可以解决这个问题。
volatile bool _complete.
Volatile关键字会指导编译器自动的为读写字段加屏障.以下是msdn的解释:
volatile 关键字指示一个字段可以由多个同时执行的线程修改。声明为 volatile 的字段不受编译器优化(假定由单个线程访问)的限制。这样可以确保该字段在任何时间呈现的都是最新的值。
使用volatile字段可以被总结成下表:
第一条指令 | 第二条指令 | 可以被交换吗? |
Read | Read | No |
Read | Write | No |
Write | Write | No(CLR会确保写和写的操作不被交换,甚至不使用volatile关键字) |
Write | Read | Yes! |
注意到应用volatile关键字,并不能保证写后面跟读的操作不被交换,这有可能会造成莫名其妙的问题。例如:
复制代码 代码如下:
volatile int x, y;
void Test1()
{
x = 1; //Volatile write
int a = y; //Volatile Read
}
void Test2()
{
y = 1; //Volatile write
int b = x; //Volatile Read
}
如果Test1和Test2在不同的线程中并发执行,有可能a 和b 字段的值都是0,(尽管在x和y上应用了volatile 关键字)
这是一个避免使用volatile关键字的好例子,甚至假设你彻底的明白了这段代码,是不是其他在你的代码上工作的人也全部明白呢?。
在Test1 和Test2方法中使用完全栅栏或者是lock都可以解决这个问题,
还有一个不使用volatile关键字的原因是性能问题,因为每次读写都创建了内存栅栏,例如
复制代码 代码如下:
volatile m_amount
m_amount = m_amount + m_amount.
Volatile 关键字不支持引用传递的参数,和局部变量。在这样的场景下,你必须使用
VolatileRead和VolatileWrite方法。例如
复制代码 代码如下:
volatile int m_amount;
Boolean success =int32.TryParse(“123”,out m_amount);
//生成如下警告信息:
//cs0420:对volatile字段的引用不被视为volatile.
VolatileRead 和VolatileWrite
从技术上讲,Thread类的静态方法VolatileRead和VolatileWrite在读取一个 变量上和volatile 关键字的作用一致。
他们的实现是一样是低效率的,尽管事实上他们都创建了内存栅栏。下面是他们在integer类型上的实现。
复制代码 代码如下:
public static void VolatileWrite(ref int address, int value)
{
Thread.MemoryBarrier(); address = value;
}
public static int VolatileRead(ref int address)
{
int num = address; Thread.MemoryBarrier(); return num;
}
你可以看到如果你在调用VolatileWrite之后调用VolatileRead,在中间没有栅栏会被创建,这同样会导致我们上面讲到写之后再读顺序可能变换的问题。
栏目列表
最新更新
nodejs爬虫
Python正则表达式完全指南
爬取豆瓣Top250图书数据
shp 地图文件批量添加字段
爬虫小试牛刀(爬取学校通知公告)
【python基础】函数-初识函数
【python基础】函数-返回值
HTTP请求:requests模块基础使用必知必会
Python初学者友好丨详解参数传递类型
如何有效管理爬虫流量?
SQL SERVER中递归
2个场景实例讲解GaussDB(DWS)基表统计信息估
常用的 SQL Server 关键字及其含义
动手分析SQL Server中的事务中使用的锁
openGauss内核分析:SQL by pass & 经典执行
一招教你如何高效批量导入与更新数据
天天写SQL,这些神奇的特性你知道吗?
openGauss内核分析:执行计划生成
[IM002]Navicat ODBC驱动器管理器 未发现数据
初入Sql Server 之 存储过程的简单使用
这是目前我见过最好的跨域解决方案!
减少回流与重绘
减少回流与重绘
如何使用KrpanoToolJS在浏览器切图
performance.now() 与 Date.now() 对比
一款纯 JS 实现的轻量化图片编辑器
关于开发 VS Code 插件遇到的 workbench.scm.
前端设计模式——观察者模式
前端设计模式——中介者模式
创建型-原型模式