从ObjectPool到CAS指令

相信最近看过我的文章的朋友对于 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 _policy;

// 是否默认的策略 是一个IL优化 使编译器生成call 而不是 callvirt
private protected readonly bool _isDefaultPolicy;

// 因为池化大多数场景只会获取一个对象 为了性能考虑 单独整一个对象不放在数组中
// 避免数组遍历
private protected T? _firstItem;

// 这个类是在2.1中引入的,以尽可能地避免接口调用 也就是去虚拟化 callvirt
private protected readonly PooledObjectPolicy? _fastPolicy;

构造方法

另外就是它的构造方法,默认实现 DefaultObjectPool有两个构造函数,代码如下所示:

///
/// Creates an instance of .
///
/// The pooling policy to use.

public DefaultObjectPool(IPooledObjectPolicy policy)
    : this(policy, Environment.ProcessorCount * 2)
{
    // 从这个构造方法可以看出,如果我们不指定ObjectPool的池大小
    // 那么池大小会是当前可用的CPU核心数*2
}

///
/// Creates an instance of .
///
/// The pooling policy to use.

/// The maximum number of objects to retain in the pool.

public DefaultObjectPool(IPooledObjectPolicy policy, int maximumRetained)
{
    _policy = policy ?? throw new ArgumentNullException(nameof(policy));

    // 是否为可以消除callvirt的策略
    _fastPolicy = policy as PooleObjectPolicy;
    // 如上面备注所说 是否为默认策略 可以消除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 需要调用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 (ref T location1, T value, T comparand)
where T : class;

一个简单的流程如下所示:

从ObjectPool到CAS指令
简单的使用代码如下所示:
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的时候,才会交换成功:

从ObjectPool到CAS指令

那么 Interlocked.CompareExchange是如何做到原子性的?在多核CPU中,数据可能在内存或者L1、L2、L3中(如下图所示),我们如何保证能原子性的对某个数据进行操作?

从ObjectPool到CAS指令

实际上这是CPU提供的功能,如果查看过JIT编译的结果,可以看到 CompareExchange是由一条叫 lock cmpxchgl的汇编指令支撑的。

从ObjectPool到CAS指令

其中 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指令。

从ObjectPool到CAS指令

总结

本文主要是带大家看了下 ObjectPool的源码,然后看了看 ObjectPool能实现无锁线程安全的最大功臣 Interlocked.CompareExchange方法;然后通过汇编代码了解了一下 Interlocked类中的一些方法是如何做到 原子性的。

感谢阅读,如果您觉得本文还不错,欢迎点赞、转发+评论,您的支持是我更新的动力!

Original: https://www.cnblogs.com/InCerry/p/objectpool_and_cas.html
Author: InCerry
Title: 从ObjectPool到CAS指令

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/799454/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 数据分析与处理—Numpy

    Numpy是目前Python数值计算中最为重要的基础包。大多数计算包都提供了基于Numpy的科学函数功能,将Numpy的数组对象作为数据交换的通用语。 在数据分析过程中,要关注: …

    Python 2023年8月25日
    067
  • 大数据时代,数据仓库究竟是干嘛的?

    前言 无论你是否专门从事大数据开发,作为一个开发人员,应该都听说过数据仓库的概念,那你知道为什么会出现数据仓库?数据仓库究竟是干嘛的吗?有什么价值和意义呢?那么本文就带到入门,揭开…

    Python 2023年9月26日
    048
  • python—控制网址界面大小

    python—控制网址界面大小 原创 夕陌2022-07-19 11:26:07©著作权 文章标签 百度 搜索 自定义 文章分类 Python 后端开发 ©著作权归作者…

    Python 2023年5月25日
    074
  • 04-pytest第三方插件

    一、pytest-ordering:调整测试用例的执行顺序 安装:pip instasll pytest-ordering @pytest.mark.last(最后一个执行); @…

    Python 2023年9月14日
    044
  • 关于跳跃连接 卷积网络

    在训练深度神经网络时,模型的性能随着架构深度的增加而下降。这被称为退化问题。 网络深度的增加,模型的性能下降的原因可能是什么? a,过度拟合。随着深度的增加,模型往往会过度拟合 b…

    Python 2023年10月10日
    038
  • pythongui界面直方图_Python pyplot可视化直方图的绘制,pythonpyplot,之,柱状图

    可视化操作是工程师必备技能。在算法开发中,可视化能够辅助调试。本文对pyplot绘制柱形图做记录。 今天我欲对se_net中权重的学习情况进行可视化研究,从而判断出哪个通道/特征占…

    Python 2023年8月9日
    051
  • matplotlib

    import numpy as np import matplotlib.pyplot as plt #准备数据 ypoints=np.array([5,10,15,20,25,3…

    Python 2023年9月1日
    048
  • [paper] lift,splat,shooting 论文浅析

    目录 00 前言 01 创新点 02 算法细节 Lift:潜在深度分布: Splat: Pillar Pooling(支柱池) Shoot: Motion Planning 03 …

    Python 2023年9月28日
    041
  • Javascript 手写 LRU 算法

    LRU 是 Least Recently Used 的缩写,即最近最少使用。作为一种经典的缓存策略,它的基本思想是长期不被使用的数据,在未来被用到的几率也不大,所以当新的数据进来时…

    Python 2023年10月20日
    028
  • 基于iNeuOS工业互联网平台的板材实时质检系统

    1. 项目背景 刨花板生产线由于原料、生产工艺等原因,会有一些产品板面出现颤纹、漏砂、胶斑、胶块、大刨花、粉尘斑、板面划痕和油污等缺陷。表面缺陷会降低板材强度、影响板材外观和二次加…

    Python 2023年10月15日
    041
  • Python学习:绑定方法与非绑定方法

    类中定义的函数分为两大类:绑定方法和非绑定方法 其中绑定方法又分为绑定到对象的对象方法和绑定到类的类方法。 在类中正常定义的函数默认是绑定到对象的,而为某个函数加上装饰器@clas…

    Python 2023年10月31日
    045
  • Python Flask教程学习01

    教程来源于w3cschool,我跟着敲一遍,做一遍 Flask是一个轻量级的可定制框架,使用Python语言编写,较其他同类型框架更为灵活、轻便、安全且容易上手。它可以很好地结合M…

    Python 2023年8月13日
    064
  • 强化学习-学习笔记4 | Actor-Critic

    Actor-Critic 是价值学习和策略学习的结合。Actor 是策略网络,用来控制agent运动,可以看做是运动员。Critic 是价值网络,用来给动作打分,像是裁判。 4. …

    Python 2023年10月29日
    061
  • 【无标题】工商银行科技菁英岗笔经面经

    工商银行2022夏季招聘笔经面经 投递信息 笔试 面试 本文还发布于牛客网 投递信息 应聘岗位 : 科技菁英-珠海-后端开发工程师投递渠道: 官网( 中国工商银行人才招聘 (icb…

    Python 2023年9月16日
    097
  • Python 最有用的25个代码段

    前言 Python是一种通用的高级编程语言。用它可以做许多事,比如开发桌面 GUI 应用程序、网站和 Web 应用程序等。 作为一种高级编程语言,Python 还可以让你通过处理常…

    Python 2023年5月24日
    0121
  • 四、Python数据挖掘(Pandas库)

    四、Python数据挖掘(Pandas库) 目录: 四、Python数据挖掘(Pandas库) * – + 一、Pandas 简介 二、Pandas 三大结构之——Da…

    Python 2023年8月17日
    078
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球