详解从ObjectPool到CAS指令
作者:InCerry 发布时间:2022-04-26 16:38:05
相信最近看过我的文章的朋友对于Microsoft.Extensions.ObjectPool
不陌生;复用、池化是在很多高性能场景的优化技巧,它能减少内存占用率、降低GC频率、提升系统TPS和降低请求时延。
那么池化和复用对象意味着同一时间会有多个线程访问池,去获取和归还对象,那么这肯定就有并发问题。那ObjectPool
在涉及多线程访问资源应该怎么做到线程安全呢?
今天就带大家通过学习ObjectPool
的源码聊一聊它是如何实现线程安全的。
源码解析
ObjectPool
的关键就在于两个方法,一个是Get
用于获取池中的对象,另外就是Return
用于归还已经使用完的对象。我们先来简单的看看ObjectPool
的默认实现DefaultObjectPool.cs
类的内容。
私有字段
先从它的私有变量开始,下面代码中给出,并且注释了其作用:
// 用于存放池化对象的包装数组 长度为构造函数传入的max - 1
// 为什么 -1 是因为性能考虑把第一个元素放到 _firstItem中
private protected readonly ObjectWrapper[] _items;
// 池化策略 创建对象 和 回收对象的防范
private protected readonly IPooledObjectPolicy<T> _policy;
// 是否默认的策略 是一个IL优化 使编译器生成call 而不是 callvirt
private protected readonly bool _isDefaultPolicy;
// 因为池化大多数场景只会获取一个对象 为了性能考虑 单独整一个对象不放在数组中
// 避免数组遍历
private protected T? _firstItem;
// 这个类是在2.1中引入的,以尽可能地避免接口调用 也就是去虚拟化 callvirt
private protected readonly PooledObjectPolicy<T>? _fastPolicy;
构造方法
另外就是它的构造方法,默认实现DefaultObjectPool
有两个构造函数,代码如下所示:
/// <summary>
/// Creates an instance of <see cref="DefaultObjectPool{T}"/>.
/// </summary>
/// <param name="policy">The pooling policy to use.</param>
public DefaultObjectPool(IPooledObjectPolicy<T> policy)
: this(policy, Environment.ProcessorCount * 2)
{
// 从这个构造方法可以看出,如果我们不指定ObjectPool的池大小
// 那么池大小会是当前可用的CPU核心数*2
}
/// <summary>
/// Creates an instance of <see cref="DefaultObjectPool{T}"/>.
/// </summary>
/// <param name="policy">The pooling policy to use.</param>
/// <param name="maximumRetained">The maximum number of objects to retain in the pool.</param>
public DefaultObjectPool(IPooledObjectPolicy<T> policy, int maximumRetained)
{
_policy = policy ?? throw new ArgumentNullException(nameof(policy));
// 是否为可以消除callvirt的策略
_fastPolicy = policy as PooleObjectPolicy<T>;
// 如上面备注所说 是否为默认策略 可以消除callvirt
_isDefaultPolicy = IsDefaultPolicy();
// 初始化_items数组 容量还剩一个在 _firstItem中
_items = new ObjectWrapper[maximumRetained - 1];
bool IsDefaultPolicy()
{
var type = policy.GetType();
return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(DefaultPooledObjectPolicy<>);
}
}
Get 方法
如上文所说,Get()
方法是ObjectPool
中最重要的两个方法之一,它的作用就是从池中获取一个对象,它使用了CAS
近似无锁的指令来解决多线程资源争用的问题,代码如下所示:
public override T Get()
{
// 先看_firstItem是否有值
// 这里使用了 Interlocked.CompareExchange这个方法
// 原子性的判断 _firstItem是否等于item
// 如果等于那把null赋值给_firstItem
// 然后返回_firstItem对象原始的值 反之就是什么也不做
var item = _firstItem;
if (item == null || Interlocked.CompareExchange(ref _firstItem, null, item) != item)
{
var items = _items;
// 遍历整个数组
for (var i = 0; i < items.Length; i++)
{
item = items[i].Element;
// 通过原子性的Interlocked.CompareExchange尝试读取一个元素
// 读取成功则返回
if (item != null && Interlocked.CompareExchange(ref items[i].Element, null, item) == item)
{
return item;
}
}
// 如果遍历整个没有获取到元素
// 那么走创建方法,创建一个
item = Create();
}
return item;
}
上面代码中,有一个点解释一下Interlocked.CompareExchange(ref _firstItem, null, item) != item
,其中!=item
,如果其等于item
就说明交换成功了,当前线程获取到_firstItem
元素的期间没有其它线程修改_firstItem
的值。
Return 方法
Retrun(T obj)
方法是ObjectPool
另外一个重要的方法,它的作用就是当程序代码把从池中获取的对象使用完以后,将其归还到池中。同样,它也使用CAS
指令来解决多线程资源争用的问题,代码如下所示:
public override void Return(T obj)
{
// 使用策略的Return方法对元素进行处理
// 比如 List<T> 需要调用Claer方法清除集合内元素
// StringBuilder之类的也需要调用Claer方法清除缓存的字符
if (_isDefaultPolicy || (_fastPolicy?.Return(obj) ?? _policy.Return(obj)))
{
// 先尝试将归还的元素赋值到 _firstItem中
if (_firstItem != null || Interlocked.CompareExchange(ref _firstItem, obj, null) != null)
{
var items = _items;
// 如果 _firstItem已经存在元素
// 那么遍历整个数组空间 找一个存储为null的空位将对象存储起来
for (var i = 0; i < items.Length && Interlocked.CompareExchange(ref items[i].Element, obj, null) != null; ++i)
{
}
}
}
}
从核心的Get()
和Set()
方法来看,其实整个代码是比较简单的,除了有一个_firstItem
有一个简单的优化,其余没有什么特别的复杂的逻辑。
主要的关键就在Interlocked.CompareExchange
方法上,我们在下文来仔细研究一下这个方法。
关于 Interlocked.CompareExchange
Interlocked.CompareExchange
它实际上是一个CAS
的实现,也就是Compare And Swap,从名字就可以看出来,它就是比较然后交换的意思。
从下面的代码段我们也可以看出来,它总共需要三个参数。其特性就是只有当localtion1 == comparand
的时候才会将value
赋值给localtion1
,另外吧localtion1
的原始值返回出来,这些操作都是原子性的。
// localtion1 需要比较的引用A
// value 计划给引用A 赋的值
// comparand 和引用A比较的引用
public static T CompareExchange<T> (ref T location1, T value, T comparand)
where T : class;
一个简单的流程如下所示:
简单的使用代码如下所示:
var a = 1;
// a == 1的话就将其置为0
// 判断是否成功就看返回的值是否为a的原始值
if(Interlocked.CompareExchange(ref a, 0, 1) == 1)
Console.WriteLine("1.成功");
// 现在a已经变为0 这个交换不会成功
if(Interlocked.CompareExchange(ref a, 0, 1) == 1)
Console.WriteLine("2.成功");
结果如下所示,只有当a
的原始值为1
的时候,才会交换成功:
那么Interlocked.CompareExchange
是如何做到原子性的?在多核CPU中,数据可能在内存或者L1、L2、L3中(如下图所示),我们如何保证能原子性的对某个数据进行操作?
实际上这是CPU提供的功能,如果查看过JIT编译的结果,可以看到CompareExchange
是由一条叫lock cmpxchgl
的汇编指令支撑的。
其中lock
是一个指令前缀,汇编指令被lock
修饰后会成为"原子的",lock
指令有两种实现方法:
早期 - Pentium时代(锁总线),在Pentium及之前的处理器中,带有
lock
前缀的指令在执行期间会锁住总线,使得其它处理器暂时无法通过总线访问内存,很显然,这个开销很大。现在 - P6以后时代(锁缓存),在新的处理器中,Intel使用缓存锁定来保证指令执行的原子性,缓存锁定将大大降低lock前缀指令的执行开销。
现在这里的锁缓存(Cache Locking)就是用了Ringbus + MESI协议。
MESI
协议是 Cacheline 四种状态的首字母的缩写,分别是修改(Modified)态、独占(Exclusive)态、共享(Shared)态和失效(Invalid)态。 Cache 中缓存的每个 Cache Line 都必须是这四种状态中的一种。
修改态(Modified),如果该 Cache Line 在多个 Cache 中都有备份,那么只有一个备份能处于这种状态,并且“dirty”标志位被置上。拥有修改态 Cache Line 的 Cache 需要在某个合适的时候把该 Cache Line 写回到内存中。但是在写回之前,任何处理器对该 Cache Line在内存中相对应的内存块都不能进行读操作。 Cache Line 被写回到内存中之后,其状态就由修改态变为共享态。
独占态(Exclusive),和修改状态一样,如果该 Cache Line 在多个 Cache 中都有备份,那么只有一个备份能处于这种状态,但是“dirty”标志位没有置上,因为它是和主内存内容保持一致的一份拷贝。如果产生一个读请求,它就可以在任何时候变成共享态。相应地,如果产生了一个写请求,它就可以在任何时候变成修改态。
共享态(Shared),意味着该 Cache Line 可能在多个 Cache 中都有备份,并且是相同的状态,它是和内存内容保持一致的一份拷贝,而且可以在任何时候都变成其他三种状态。
失效态(Invalid),该 Cache Line 要么已经不在 Cache 中,要么它的内容已经过时。一旦某个Cache Line 被标记为失效,那它就被当作从来没被加载到 Cache 中。
总得来说,若干个CPU核心通过Ringbus连到一起。每个核心都维护自己的Cache的状态。如果对于同一份内存数据在多个核里都有Cache,则状态都为S(Shared)。
一旦有一核心改了这个数据(状态变成了M),其他核心就能瞬间通过Ringbus感知到这个修改,从而把自己的Cache状态变成I(Invalid),并且从标记为M的Cache中读过来。同时,这个数据会被原子的写回到主存。最终,Cache的状态又会变为S。
关于MESI
协议更详细的信息就不在本文中介绍了,在计算机操作系统和体系结构相关书籍和资料中有更详细的介绍。
然后compxchg
这个指令就很简单了,和我们之前提到的一样,比较两个地址中的值是否相等,如果相等的话那么就修改。
Interlocked
类中的其它方法也是同样的原理,我们可以看看Add
之类的方法,同样是在对应的操作指令前加了lock
指令。
来源:https://www.cnblogs.com/InCerry/p/objectpool_and_cas.html
猜你喜欢
- spring与IoCIoC:控制反转,将由代码操纵的对象控制权,交给第三方容器,反转给第三方容器。这种对象依赖的关系管理方式,称作IoC。I
- 朋友让我帮忙写个程序从文本文档中导入数据到oracle数据库中,技术上没有什么难度,文档的格式都是固定的只要对应数据库中的字段解析就行了,关
- 一,网络编程中两个主要的问题一个是如何准确的定位网络上一台或多台主机,另一个就是找到主机后如何可靠高效的进行数据传输。在TCP/IP协议中I
- MybatisPlus特性•无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑•损耗小:启动即会自动注入基本 CURD,性能
- 本文实例讲述了C#双缓冲实现方法。分享给大家供大家参考,具体如下:// 该调用是 Windows.Forms &nb
- 如下所示:public class 字符串常用操作 { public static void main(String[] arg
- 定义:当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知它的依赖对象。特
- 讲完了inbound事件和outbound事件的传输流程, 这一小节剖析异常事件的传输流程传播异常事件简单的异常处理的场景@Override
- 身为一名开发人员,大家都知道,我们经常会在项目中大量的编写许多重复的代码,比如说public Entity find(String id);
- 背景介绍1,最近有一个大数据量插入的操作入库的业务场景,需要先做一些其他修改操作,然后在执行插入操作,由于插入数据可能会很多,用到多线程去拆
- 使用mybatis-plus自动生成了5个模块(xml/bean/mapper/service/controller)的代码,这里练习一下m
- 首先在pom文件里引入mqtt的依赖配置<!--mqtt--> <d
- 整理文档,搜刮出一个android 通过MediaRecorder实现简单的录音示例,稍微整理精简一下做下分享。MainActivitypa
- 因为mybatis好使,所以几乎需要操作数据库的时候,我都会使用mybatis,而且在一个正式的项目中,同时存在BS和CS的程序,都使用的M
- Android Bitmap和Drawable的对比Bitmap - 称作位图,一般位图的文件格式后缀为bmp,当然编码器也有很多如RGB5
- 最近做了微信公众号支付的开发,由于是第一次做也摸索了几天的时间,也只是达到了实现功能的水平,并没有太多考虑到性能问题,所以这篇文章比较适合初
- 前言前面的例子都是多个线程在做相同的操作,比如4个线程都对共享数据做tickets–操作。大多情况下,程序中需要不同的线程做不同的事,比如一
- 本文实例为大家分享了Java实现坦克大战小游戏的具体代码,供大家参考,具体内容如下创作背景:n年前的学期末课题设计,从b站上学的,一个代码一
- 前言Kotlin一个强大之处就在于它的扩展函数,巧妙的运用这些扩展函数可以让你写出的代码更加优雅,阅读起来更加流畅,下面总结了在开发中经常用
- 前言springmvc中有两种很普遍的AOP实现:1.过滤器(Filter)2. * (Interceptor)本篇面对的是一些刚接触spr