ABA问题

2023-11-06

这篇文章(http://oceanbase.org.cn/?p=82)的第6小节讲述了Hazard Version的实现原理,它的设计思想最早由OB团队的席华锋提出,本文不再赘述,本文主要分享Hazard Version的实现要点,以及使用它实现无锁StackQueue的方法,已经在多核系统上的性能测试,代码已在github共享。

  • ABA问题

如《共享变量的并发读写》一文所述,Hazard Version主要解决的是无锁数据结构处理内存释放的问题,为此我们先来回顾一下无锁编程领域最经典的“ABA问题”,使用链表实现的无锁Stack来举例:

使用一个top指针来指向栈顶,使用CAS原子操作对top进行修改,来实现pushpop,一个常见但错误的实现可能是这样的:

void push(Node *node) {
    Node *curr = top;
    Node *old = curr;
    node→next = curr;
    while (old != (curr = CAS(&top, old, node))) {
        old = curr;
	node→next = curr;		
    }
}
Node *pop() {
    Node *curr = top;
    Node *old = curr;
    while (NULL != curr
        old != (curr = CAS(&top, old, curr→next))) {
	old = curr;
    }
    return curr
}

push的逻辑没问题,问题在于pop,它在还未“锁定”top的情况下,就引用了topnext字段,而此时的top指向的内存空间可能已经被释放甚至重用,一方面可能直接引起非法地址访问使得程序直接崩溃;更严重的可能造成隐蔽的“ABA问题”,考虑如下时序:

  1. Stack状态为 ABCtop指向A

  2. 线程Itop(即节点A的地址)赋值给curr,并取得curr→next指针(为B)放入寄存器

  3. 线程II执行完整pop流程,Stack状态变为BCtop指向B,节点A被弹出后释放

  4. 线程II执行完整pop流程,Stack状态变为Ctop指向C,节点B被弹出后释放

  5. 线程II执行完整push流程,新申请节点为被复用的AStack状态变为ACtop指向A

  6. 线程I继续执行,CAS判断top值仍为A,则原子替换topB,链表被改坏...

我们无法通过简单的修改pop逻辑来避免“ABA问题”,因为这里的纠结之处在于要“锁定(取出)”top,就要使用CAS原子的将top改为它的next指针,但是要安全的引用top→next又要先“锁定(取出)”top。因此解决“ABA问题”的根本,在于保证pop逻辑引用top指针时,它不会被释放,而在释放节点内存空间时,严格保证不会再有其他人引用这个节点。Hazard PointerMichael M M. Hazard pointers: Safe memory reclamation for lock-free objects[J]. IEEE Transactions on Parallel and Distributed Systems, 2004, 15(6): 491-504.)技术是一个里程碑式的创新,真正在实际工程中解决“ABA问题”(memsql/oceanbase都在使用),Hazard Version是对Hazard Pointer在易用性上的改进,原理一致,详细介绍请参考《共享变量的并发读写》一文。

  • Hazard Version实现要点和局限

    • Hazard Version设计概要


      如上图所示,Hazard Version数据结构主要由三部分组成

      • 全局的Current Version变量

      • 每个线程局部一个Hazard Version变量

      • 每个线程局部一个待回收的内存块链表Retire List

      Hazard Version提供如下三个操作逻辑:

      • 每个线程要开始访问“可能被其他线程释放”的内存块前,将当前Current Version的值保存在线程局部的Hazard Version中,对共享内存块的操作完成后,再清除线程局部的Hazard Version值。

      • 要释放一个共享内存块时,原子的将Current Version1后,将旧值保存在内存块中,然后将它挂在线程局部的Retire List上。

      • 当待回收的内存块过多时,遍历所有线程的Hazard Version,以及全局的Current Version,获得最小值(Min Version),遍历待回收的内存块,将Version小于Min Version的内存块回收掉。

    • 获取Min Version的原子性问题

      如上所述,遍历所有线程,获取Min Version的操作并不保证原子性,可能出现一个线程获取到一个很小的Current Version后,被切换出去,而错过了同时进行的遍历操作。这个问题可以通过简单的double check来规避,线程拿到Current Version,保存到线程局部的Hazard Version后,再检查一下当前Current Version是否发生变化,如果已发生变化,则重试直到Current Version不再改变为止。因为如果Current Version未发生变化,即使遍历被错过,Current Version也参加了遍历,所以得到的Min Version就是真正的Hazard Version最小值。

    • 无锁遍历Retire List

      回收内存时,每个线程优先回收自己本地的Retire List,如果全局待回收的节点仍然比较多,则遍历其他线程的Retire List进行回收,这里会涉及多线程操作Retire List(即它的owner线程挂入新节点,而其他线程要进行遍历),一个简单的做法是要遍历Retire List时,使用CASRetire List替换为NULL,取得旧值后,在当前线程本地进行遍历,对遍历剩下的节点,就挂到线程本地的Retire List上去。

    • CAS小技巧

      CAS操作通常在循环中执行,一次CAS执行失败后,要重新准备新的compare参数和assign参数。比如原子修改栈顶:

      Node *old_v = top;
      
      node→next = old_v;
      
      while (old_v != CAS(&top, old_v, node)) {
      
      old_v = top;
      
      node→next = old_v;
      
      }

      因为CAS无论成功与否,都可以返回旧值,因此这里可以用一个临时变量保存CAS的返回值,避免重新取一次top,毕竟重新取top很可能造成缓存miss

      Node *curr_v = top;
      
      Node *old_v = curr_v;
      
      node→next = curr_v;
      
      while (old_v != (curr_v = CAS(&top, old_v, node))) {
      
      old_v = curr_v;
      
      node→next = curr_v;
      
      }
    • Hazard Version的局限

      相对Hazard Pointer来说,Hazard Version这个本质上是管理多个线程Hazard Version的“滑动窗口”,它通过增加获取Min Version的代价,来减小各个线程获取和重置Hazard Version的代价。因此它有与“滑动窗口”一样的局限性,太久不释放的Hazard Version,会阻塞内存回收。在使用时需要注意,对Hazard Versionrequirerelease操作之间,不能有耗时过长的逻辑。

    • Hazard Version代码仓库:

      https://github.com/kayaklee/libhalog/blob/master/src/clib/hal_hazard_version.h

  • 无锁Stack

    使用Hazard Version实现无锁Stack比较简单,因为push不涉及“ABA问题”,所以只需要在pop时使用Hazard Version保护即可。

    代码请参考:https://github.com/kayaklee/libhalog/blob/master/test/clib/hv_sample_lifo.cpp

    性能测试结果:如下图所示,在美团云12核虚拟机上,6个线程push6个线程pop,不带pop结果检查情况下,最快接近780/spush+pop次数。

     

  • 无锁Queue

    Stack不同,实现无锁Queue的一个难点在要维护HeadTail两个指针,在Queue由空变为非空,和非空变为空的时候,需要保证原子的修改和判断这两个指针,这几乎是没有办法实现的。一个变通的方法,是受到Hazard Pointer论文的启发,在初始状态下,引入一个Dummy Header节点,无论push还是pop,都始终保证headtail不会变为NULL

    这样一来,push操作不需要管head指针,每次都只修改tail即可;而pop操作,是取head→next节点中的值作为队首数据,确认head→next不为空,然后原子的将head改为head→next。因为pushpop操作都存在先拿tailhead指针,然后引用它们的next成员的操作,因此都需要使用Hazard Version进行保护。

    代码请参考:https://github.com/kayaklee/libhalog/blob/master/test/clib/hv_sample_fifo.cpp

    性能测试结果:如下图所示,在美团云12核虚拟机上,6个线程push6个线程pop,不带pop结果检查情况下,最快超过1300/spush+pop次数。

  • StackQueue性能测试数据和虚拟机cpu信息:

    https://github.com/kayaklee/libhalog/tree/master/test/clib/hv_performance_record

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

ABA问题 的相关文章

  • Customplot画多条折线图,同时可以控制每条曲线的隐藏和显示

    Customplot多条曲线的控制 前言 开始使用Qcharts画图 大数据性能极差 于是转用Customplot画图 主要进行数据的实时更新和大量数据的加载 一 模拟数据 采用子线程创建模拟数据 采用队列存储 pragma once in
  • PostgreSQL简单使用介绍

    之前没怎么接触各类数据库 现在对新上手的数据库都来学习一番 项目组经常用到的数据库和新使用的数据库都会做个笔记 本篇讲讲postgresql 1 安装配置postgresql 参考网址 https blog csdn net DaSo CS
  • LINUX驱动开发学习笔记---GCC编译器

    一 GCC编译器基础使用 Q 为什么需要GCC编译器 A 在Windows下我们 可以 使用各种各样的 IDE进行编程 比如强大的 Visual Studio 它既可以编辑也可以编译 但是linux下vi或vim编辑器只能用于编辑 不能编译
  • 【机器学习】最大熵算法 整理

    最大熵模型由最大熵原理推导实现 1 最大熵原理 最大熵原理认为 学习概率模型时 在所有可能的概率模型 分布 中 熵最大的模型是最好的模型 通常用约束条件来确定概率模型的集合 所以 最大熵原理也可以表述为在满足约束条件的模型集合中选取熵最大的
  • Python如何把 dict 快速转换为namedtuple

    下面的代码可能让你更容易理解
  • Maven实战(三)Eclipse构建Maven项目

    1 Eclipse安装m2eclipse插件 见eclipse maven plugin 插件 安装 和 配置 2 构建Maven项目 2 1 创建简单Maven项目 点击Eclipse菜单栏File gt New gt Ohter gt
  • FastApi-21-APIRouter

    Part1背景 通常在我们开发 app 时都会用到路由 像 Flask 有 blueprint Django 有 urls 等 其目的都是为了路由汇总管理 FastApi 也不例外 其拥有 APIRouter 今天我们就一起来了解 APIR
  • vue分层项目架构搭建过程与踩过的坑

    项目介绍 公司已有saas项目 因为需求变动前后端都相应的做出架构调整 后端采用分层模式开发 要求每个模块可单独发布 可按客户需求单模块部署到客户服务器 所以前端的框架要求也要符合这个需求 前端具体需求 1 客户有自己的系统需要引入我们产品
  • 【Unity实用小知识点】EventTrigger在3D物体或UI上应用

    Event Trigger Event Trigger可以在一些简单交互上非常方便的使用 废话比较多 想直接看UI和3D区别的直接跳到总结 官方API 描述 从 EventSystem 接收事件并为每个事件调用注册函数 EventTrigg
  • 这几天来重学Java的感受

    拿出课本重新开始看 最大的感受就是以前学的太浅显了 而且缺少练习 才过了不到一年就忘得差不多了 已经下定决心要好好学习Java 不会轻言放弃 我不知道大家选择开发选择敲代码是不是真的喜欢 反正我并不是特别喜欢 不过也不算讨厌 我总觉得不管哪
  • Matlab使用CUDA--利用cudamex

    目录 一 编写可供Matlab编译的CUDA代码 1 待编译的程序需要包含的头文件 2 待编译程序的程序入口函数mexFunction 3 参数传递方法 二 使用Matlab编译CUDA工程并调用 1 mexcuda编译指令 2 参考文章
  • centos7 安装gitlab 之 被502支配的恐惧

    之前重装了下gitlab 本以为很轻松 结果pp打脸 一直就是下面这个页面 看到这个502都有阴影了 看了网上各位兄dei的写的相关问题解决办法 总结了下 1 端口被占用 etc gitlab gitlab rb 这个文件里面 有3个地方需
  • 【Step1】Java SE Development Kit 17.0.6

    点击下方链接 Java SE 17 Archive Downloads 选择下载文件 以windows x64 installer为例 运行安装文件 点下一步 可选 更改安装文件夹 点下一步 可选 点击后续步骤 JDK 17 Documen
  • Python反转输出正整数

    题目 获得输入正整数 N 反转输出该正整数 不考虑异常情况 输入格式 输入一个正整数 输出格式 输出一个正整数 疑问 为什么我的两个答案都没通过Python二级在线评阅的测试 我

随机推荐

  • 【数据库】--- Redis

    Redis 概述 Redis 简介 下载与安装 基本使用 基本知识 数据结构 字符串类型 String 列表类型 List 集合类型 Set 哈希类型 hash 有序集合 zset srted set 关于key的指令 1 查询符合条件的
  • js逆向-某动网演出数据获取

    声明 本文仅供学习参考 如有侵权可私信本人删除 请勿用于其他途径 违者后果自负 如果觉得文章对你有所帮助 可以给博主点击关注和收藏哦 前言 目标网站 aHR0cHM6Ly93d3cuc2hvd3N0YXJ0LmNvbS9ldmVudC9sa
  • 爆改闲置主机为nas

    目录 一 工具准备 1 工具 2 下载需要安装的文件 二 进行实操 1 刷U盘或者硬盘的引导 2 上x86主机 3 连接x86主机 4 安装群辉 三 进入系统 1 存储池的设置 2 共享文件夹的设置 3 用户的设置 4 IP地址的固定 作者
  • 轿车双横臂式独立前悬架及多连杆式独立后悬架设计(毕业论文+7张CAD图纸)

    轿车双横臂式独立前悬架及多连杆式独立后悬架设计 摘 要 悬架是汽车重要的组成部分 是传递车轮与车身之间的各种力和力矩的连接装置 轿车的前悬架采用的双横臂式独立悬架 其后悬采用的是多连杆式独立悬架 双横臂式的独立悬架是常见的悬架形式之一 由于
  • 详解TCP,三次握手,四次挥手

    前言 TCP是非常常见的面试题 是必会的知识点 记录一下与各位共同学习 三次握手 问 为什么要三次握手 因为三次握手才能保证双方具有接收和发送的能力 第一次握手 客户端发送带有 SYN 标志的连接请求数据包给服务端 第二次握手 服务端发送带
  • 解决linux根目录磁盘空间不足问题

    问题 一开始创建虚拟机 分配给虚拟机的磁盘空间太小了 所以磁盘空间很快就会填满 如果根目录的磁盘空间占用超过90 会导致无法再新安装软件 可以通过df h命令查看磁盘的剩余空间 df命令的英文全称即 Disk Free 顾名思义功能是用于显
  • 算法题记录【华为od】AI处理器组合

    题目描述 思路分析 这个题感觉更像优先级的问题 但是题目里面又没有讲清楚 不太理解 本人是按照题目描述以及自我理解做的 感觉还是不对劲 代码解析 let input 0 1 4 5 6 7 arr1 arr2 let input1 3 边界
  • 数据结构之概念与线性表

    算法 算法特征 1 有穷性 2 确定性 3 可行性 4 输入和输出 算法好坏评价 正确性 可读性 确定性 健壮性 效率和低存储 算法效率的度量 时间复杂度 空间复杂度 线性表 顺序存储 线性表 链式存储 指针实现 单链表 双链表 循环链表
  • 函数调用规范

    当高级语言函数被编译成机器码时 有一个问题就必须解决 因为CPU没有办法知道一个函数调用需要多少个 什么样的参数 即计算机不知道怎么给这个函数传递参数 传递参数的工作必须由函数调用者和函数本身来协调 为此 计算机提供了一种被称为栈的数据结构
  • QT5之信号槽概念以及实现机制

    概念 当一件事情发生之时便会发送一个信号 而槽就是一个函数 被用来响应这个信号当信号发生之时 关联方式 一个信号关联一个槽 connect Object1 SIGNAL signal Object2 SLOT slot signal1为对象
  • 如何在Linux中安装jdk?

    如何在Linux中安装jdk 学习目标 如何在Linux中安装jdk 1 先创建一个新的虚拟机 一共13步创建好虚拟机 2 配置虚拟机 3 在虚拟机中安装JDK 1 先创建一个新的虚拟机 一共13步创建好虚拟机 我使用的是VMware Wo
  • NIO群聊

    服务端 package nio import java io IOException import java net InetSocketAddress import java nio ByteBuffer import java nio
  • Python实验二 顺序结构程序设计

    1 阅读下面程序 i j 3 4 i j 2j i s i j print s s i j 3 4 i j 2j i s i j print s s i j 3 4 i j 2 j i s i j print s s 2 写出下列程序执行结
  • 腾讯云批量上传文件(前端)

    腾讯云批量上传文件 前端 前言 1 腾讯云上传文件 遍历调用上传方法 2 根据文件文件后缀名判断上传成功后 文件的回显形式 3 在腾讯云建立存储桶 需要后端配合写上传接口 线上测试 1 效果样式 如下 1 引入cos js sdk v5 j
  • 解决Mybatis-plus高版本不向后兼容的问题

    mybatis plus插件后面的版本没有兼容低版本 即 不存在低版本中EntityWrapper这个类了 而该类采用数据库表真实字段名作查询条件 这样硬编码形式确实不友好 比如如果后面数据库表中字段更名那么所有涉及到的业务都需要去修改 且
  • 2021年五一建模B赛题+思路

    背景 随着我国经济的高速发展 城市空间环境复杂性急剧上升 各种事故灾害频发 安全风险不断增大 消防救援队承担的任务也呈现多样化 复杂化的趋势 对于每一起出警事件 消防救援队都会对其进行详细的记录 某地有15个区域 分别用A B C 表示 各
  • ElasticSearch常用配置(内置账号密码修改、自定义角色自定义账号,日志定期删除等)...

    自定义内置账号 账户elastic为elasticsearch超级管理员 拥有所有权限 账户kibana用于kibana组件获取相关信息用于web展示 账户logstash system用于logstash服务获取elasticsearch
  • EasyAR脱卡方法

    首先说下大致思路 当卡片离开摄像头时间 ImageTarget Image的SetActive false 所以其子物体 model 也就不显示了 因此解决的办法就是在Target false 时间将模型放到一个合适的位置 这样就能实现脱卡
  • Fabric配置fabric-sample工程目录,并生成证书

    GitHub上的fabric sample工程 默认只有源码 缺少CA工具和加密工具 它需要从其他地方下载CA工具和加密工具 这里以fabric v1 4 0为例进行说明 步骤如下 1 下载fabric sample v1 4 0源码 官网
  • ABA问题

    这篇文章 http oceanbase org cn p 82 的第6小节讲述了Hazard Version的实现原理 它的设计思想最早由OB团队的席华锋提出 本文不再赘述 本文主要分享Hazard Version的实现要点 以及使用它实现