我的编程空间,编程开发者的网络收藏夹
学习永远不晚

详解从ObjectPool到CAS指令

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

详解从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<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指令。

总结

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

到此这篇关于从ObjectPool到CAS指令的文章就介绍到这了,更多相关ObjectPool到CAS指令内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

详解从ObjectPool到CAS指令

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

详解从ObjectPool到CAS指令

这篇文章主要介绍了从ObjectPool到CAS指令 ,本文主要是带大家看了下ObjectPool的源码,然后看了看ObjectPool能实现无锁线程安全的最大功臣Interlocked.CompareExchange方法,需要的朋友可以参考下
2022-11-13

汇编语言LDR指令和LDR伪指令详解

这篇文章主要介绍了汇编语言LDR指令和LDR伪指令详解,伪指令是用来自动拆分代码值的,会把一条语句拆分成多条语句,本文结合示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
2023-01-28

dword ptr指令详细解析

8086CPU的指令,可以处理两种尺寸的数据,byte和word。所以在机器指令中要指明,指令进行的是字操作还是字节操作
2022-11-15

unidbg-consoleDebugger快键指令详解

快键指令详解: help 帮助信息d|dis 反编译信息d0x地址 地址信息m eg:mr0 mr0s mr0 16(长度) 读取寄存器内存mOx 指定地址内存w0x
2023-08-17

Golang开发环境搭建指南:从初学到专家级详解

从入门到精通:Golang开发环境搭建详解一、引言随着Golang的日益流行,越来越多的开发者开始学习和使用Golang进行开发。为了能够更顺利地进行Golang的开发,我们需要搭建一个适合的开发环境。本文将详细介绍Golang开发环境的
Golang开发环境搭建指南:从初学到专家级详解
2024-01-29

Nginx中default_server指令问题详解

nginx的default_server指令可以定义默认的server出处理一些没有成功匹配server_name的请求,下面这篇文章主要给大家介绍了关于Nginx中default_server指令问题的相关资料,需要的朋友可以参考下
2022-12-27

详解汇编语言MOV指令

MOV(Move)指令是汇编语言中最基本的指令之一,用于将数据从一个位置复制到另一个位置。它的语法形式通常是:MOV dest, src其中,dest表示目标操作数,src表示源操作数。dest和src都可以是寄存器、内存地址或立即数。MO
2023-08-14

java虚拟机指令dup详解

本文实例为大家介绍了java虚拟机指令dup,供大家参考,具体内容如下举个例子:public class ExceptionTest{ void cantBeZero(int i) throws Exception{ throw n
2023-05-31

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录