C++深入解析锁机制与 CAS 实现

锁机制

在锁机制的应用中,乐观锁和悲观锁是两种常见的并发控制策略,它们主要在处理数据的一致性和并发操作时表现出不同的假设和实现方式。

乐观锁

乐观锁基于这样一个假设:冲突发生的概率很低,因此在数据操作过程中不会主动去锁定资源,而是在数据提交更新时才进行检查。如果发现冲突(通常是通过版本号或时间戳来检测),则操作被拒绝,通常伴随着重试机制。乐观锁适用于读操作多但写操作少的场景,因为它可以减少锁的开销,提高系统的吞吐量。

常见的乐观锁机制有CAS(Compare-And-Swap),这是一种硬件支持的乐观锁机制,用于多线程编程。它涉及三个操作数:内存位置、预期原值和新值。如果位置的当前值与预期值相匹配,就将这个位置的数据更新为新值。这通常用于实现无锁编程构建。

悲观锁

悲观锁则是假设冲突总是会发生,因此在整个数据处理过程中会主动加锁。这种锁可以通过数据库的行锁或表锁实现,或者在编程中使用互斥锁等。悲观锁通过锁定资源来防止其他操作影响到当前事务的执行,适用于写操作多的场景,但可能会引起锁竞争和降低并发性能。

悲观锁会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。

常见的悲观锁机制如下:

  • 互斥锁(Mutex):属于悲观锁的一种,因为它主动阻止多个线程同时执行共享资源的访问。
  • 读写锁(Read-Write Lock):也是悲观锁的一种,因为它通过区分读和写操作来减少锁的竞争,但仍然在资源访问前加锁。
  • 自旋锁(Spinlock):属于悲观锁的一种,用于防止线程在执行短期任务时被系统挂起。
  • 递归锁(Recursive Lock):属于悲观锁的一种,因为它允许同一线程多次获得同一个锁。
  • 条件变量(Condition Variable):通常与互斥锁一起使用,用于线程间的协调,虽然本身不是锁,但配合锁使用时属于悲观锁策略。
  • 信号量(Semaphore):可以视为悲观锁的一种广义形式,因为它通过控制资源的数量来限制线程并发访问。

悲观锁锁机制存在的问题:

  • 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
  • 一个线程持有锁会导致其它所有需要此锁的线程挂起。
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。

CAS机制

CAS,是Compare and Swap的简称,在这个机制中有三个核心的参数:

  • 主内存中存放的共享变量的值:V(一般情况下这个V是内存的地址值,通过这个地址可以获得内存中的值)
  • 工作内存中共享变量的副本值,也叫预期值:A
  • 需要将共享变量更新到的最新值:B

CAS算法原理描述

1.在对变量进行计算之前(如 ++ 操作),首先读取原变量值,称为 旧的预期值 A

2.然后在更新之前再获取当前内存中的值,称为 当前内存值 V

3.如果 A==V 则说明变量从未被其他线程修改过,此时将会写入新值 B

4.如果 A!=V 则说明变量已经被其他线程修改过,当前线程应当什么也不做。

用C语言来描述该操作

int compare_and_swap (int* reg, int oldval, int newval) 
{   
      int old_reg_val = *reg;   
      if (old_reg_val == oldval)      
               *reg = newval;   
      return old_reg_val; 
} 

变种为返回bool值形式的操作:返回 bool值的好处在于,调用者可以知道有没有更新成功

bool compare_and_swap (int *accum, int *dest, int newval)
{   
      if ( *accum == *dest ) 
      {       
           *dest = newval;       
           return true;   
      }   
      return false; 
} 

其他操作

除了CAS还有以下原子操作:

Fetch And Add,一般用来对变量做 +1 的原子操作。

<< atomic >>
function FetchAndAdd(address location, int inc) {
    int value := *location
    *location := value + inc
    return value
}

Test-and-set,写值到某个内存位置并传回其旧值。汇编指令BST。

#define LOCKED 1
 
int TestAndSet(int* lockPtr) {
    int oldValue;
 
    // Start of atomic segment
    // The following statements should be interpreted as pseudocode for
    // illustrative purposes only.
    // Traditional compilation of this code will not guarantee atomicity, the
    // use of shared memory (i.e. not-cached values), protection from compiler
    // optimization, or other required properties.
    oldValue = *lockPtr;
    *lockPtr = LOCKED;
    // End of atomic segment
 
    return oldValue;
}

Test and Test-and-set,用来实现多核环境下互斥锁,

boolean locked := false // shared lock variable
procedure EnterCritical() {
  do {
    while (locked == true) skip // spin until lock seems free
  } while TestAndSet(locked) // actual atomic locking
}

C/C++程序中CAS的实现

在 GCC 4.1 及以上版本中,提供了内置的 CAS 支持,主要通过以下两个函数实现:

bool __sync_bool_compare_and_swap (type *ptr, type oldval, type newval, ...) 
type __sync_val_compare_and_swap (type *ptr, type oldval, type newval, ...)

__sync_bool_compare_and_swap:此函数检查指针 ptr 指向的值是否等于 oldval,如果是,则将 ptr 指向的值设置为 newval。返回值为 bool 类型,表示是否成功替换。

__sync_val_compare_and_swap:此函数的功能类似于 __sync_bool_compare_and_swap,但返回的是操作前的原始值,而不是操作的成功与否。

C++11 标准引入了更为标准化的原子操作,通过atomic头文件中定义的 std::atomic 类来实现。该类提供了多个成员函数,其中两个用于 CAS 操作的是:

template< class T > bool atomic_compare_exchange_weak( std::atomic* obj,T* expected, T desired ); 
template< class T > bool atomic_compare_exchange_strong( volatile std::atomic* obj,T* expected, T desired );

atomic_compare_exchange_weak:尝试将 std::atomic 类型对象 obj 中的值与 expected 比较,如果相同,则将其替换为 desired。这个操作可能失败,即使在没有数据竞争的情况下也可能因为硬件优化而失败,因此它是弱比较交换。如果需要保证严格的原子性,则应该使用compare_exchange_strong函数。

atomic_compare_exchange_strong:与 atomic_compare_exchange_weak 类似,但其提供更强的保证,即不会因为假共享等硬件优化原因而失败。

注意,compare_exchange_strong函数保证原子性,因此它的效率可能比compare_exchange_weak低。

ABA问题及解决方案

CAS在操作的时候会检查变量的值是否被更改过,如果没有则更新值,但是带来一个问题,最开始的值是A,接着变成B,最后又变成了A。经过检查这个值确实没有修改过,因为最后的值还是A,但是实际上这个值确实已经被修改过了。

为了解决这个问题,在每次进行操作的时候加上一个版本号,每次操作的就是两个值,一个版本号和某个值,A——>B——>A问题就变成了1A——>2B——>3A。

真正要做到严谨的CAS机制,我们在compare阶段不仅要比较期望值A和地址V中的实际值,还要比较变量的版本号是否一致

带版本号的 CAS:这种方法通过将版本号或标签与数据值配对来解决ABA问题。每次更新数据时,除了改变数据本身,还会更新版本号。这样,即使数据值返回到原始值,版本号的改变也会阻止CAS操作错误地认为没有其他线程修改过数据。

一种可行的方法为:在指针后追加一个计数器,每次操作时增加计数。因此,即使一个元素(比如A)被出队并后来又有一个相同内存地址的新元素(如C)被入队,计数器的变化将防止CAS操作错误地认为头节点没有被改变。这是因为计数器的值将不匹配,CAS操作会失败,有效防止了ABA问题。

struct alignas(16) AtomicWord
{
    intptr_t p, num;
};

p存储节点指针,num存储应用计数。

对于Windows平台下,_InterlockedCompareExchange128 用于执行 128 位的原子比较和交换操作。这个函数尝试将两个 64 位值原子地比较与目标地址中的两个连续的 64 位值,如果它们匹配,则用新的两个 64 位值替换它们。

static inline bool AtomicCompareExchangeStrongExplicit(volatile AtomicWord* p, 
AtomicWord* oldval, AtomicWord newval)
{
    return _InterlockedCompareExchange128((volatile long long*)p, (long long)newval.p, 
    (long long)newval.num, (long long*)oldval) != 0;
}

函数尝试将 AtomicWord2 结构体中的 p 和 num 值原子地与给定的 oldval 进行比较。如果当前值(指针 p 指向的值)与 oldval 相匹配,则将这两个值替换为 newval 中的 lo 和 hi。如果成功,函数返回非零值,否则返回零。

对应于 Windows 的 _InterlockedCompareExchange128 的功能可以通过 GCC 提供的__atomic_compare_exchange 和 __atomic_compare_exchange_n 内置函数来实现。这些函数支持执行宽度达到 128 位的原子比较和交换操作。

效率测试

在Linux上测试,以下为无锁和有锁,模拟高并发情况。

int mutex = 0;
int lock = 0;
int unlock = 1;
//无锁
static volatile int count = 0;
void *test_func(void *arg)
{
    int i = 0;
    for(i = 0; i < 2000000; i++)
    {
        while (!(__sync_bool_compare_and_swap (&mutex,lock, 1) ))usleep(100000);
        count++;
        __sync_bool_compare_and_swap (&mutex, unlock, 0);
    }
    return NULL;
}
// 有锁
pthread_mutex_t mutex_lock;
static volatile int count = 0;
void *test_func(void *arg)
{
    int i = 0;
    for(i = 0; i < 2000000; i++)
    {
        pthread_mutex_lock(&mutex_lock);
        count++;
        pthread_mutex_unlock(&mutex_lock);
    }
    return NULL;
}

自行测试:无锁操作在性能上优于加锁操作,消耗时间仅为加锁操作的1/3左右,无锁编程方式确实能够比传统加锁方式效率高

缺点

看起来CAS比锁的效率高,从阻塞机制变成了非阻塞机制,减少了线程之间等待的时间。每个方法不能绝对的比另一个好,在线程之间竞争程度大的时候,如果使用CAS,每次都有很多的线程在竞争,也就是说CAS机制不能更新成功。这种情况下CAS机制会一直重试,这样就会比较耗费CPU。因此可以看出,如果线程之间竞争程度小,使用CAS是一个很好的选择;但是如果竞争很大,使用锁可能是个更好的选择。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/594767.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【C语言】解决不同场景字符串问题:巧妙运用字符串函数

&#x1f308;个人主页&#xff1a;是店小二呀 &#x1f308;C语言笔记专栏&#xff1a;C语言笔记 &#x1f308;C笔记专栏&#xff1a; C笔记 &#x1f308;喜欢的诗句:无人扶我青云志 我自踏雪至山巅 文章目录 一、字符函数1.1 字符分类函数1.1.1 islower1.1.2 isupper 1.…

Android中TabLayout与ViewPager结合使用生命周期详解

博主前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住也分享一下给大家&#xff0c; &#x1f449;点击跳转到教程 效果 使用的布局如下&#xff1a; <?xml version"1.0" encoding"utf-8"?> …

踏准芯片定制风口的灿芯股份,护城河足够深吗?

近年来&#xff0c;芯片定制渐成风潮&#xff0c;不仅位于下游、自身有巨大芯片需求的科技巨头如谷歌、OpenAI等纷纷转向定制&#xff0c;而且产业中游主打标准化芯片的主流芯片设计公司如博通、英伟达等&#xff0c;也相继开辟或加码定制业务。 风潮背后&#xff0c;一方面是…

【JavaEE网络】从数据链路层到应用层的DNS

目录 数据链路层以太网 DNS 数据链路层 越往下与程序员越远 代表协议&#xff1a;以太网。平常用的网线也叫“以太网线”&#xff0c;平常用的交换机也叫“以太网交换机” 以太网 认识以太网 “以太网” 不是一种具体的网络&#xff0c;而是一种技术标准&#xff1b;既包含…

Git笔记-常用指令

Git笔记-常用指令 一、概述二、仓库管理二、缓存区操作1. 添加文件到缓存区2. 取消缓存文件3. 忽略列表 三、日志状态信息四、分支操作五、六、 一、概述 这里记录一些git常用的指令。 二、仓库管理 # 本地仓库初始化 git init# 克隆仓库 git clone git_url # git clone ht…

Unity之ShaderGraph入门简介与配置

前言 ShaderGraph是Unity的一个可视化着色器编辑工具,它允许开发者在不编写代码的情况下创建复杂的着色器效果。ShaderGraph提供了一个直观的图形界面,用户可以通过拖拽节点并连接它们来构建自定义的着色器。用户可以在ShaderGraph中使用各种节点,如数学运算、纹理采样、颜…

亚马逊Lazada速卖通卖家必备:利用自养号测评提升店铺排名与销量

Wish与亚马逊、速卖通、eBay等知名的跨境电商平台有所区别&#xff0c;它专注于移动端市场。对于许多初次涉足跨境电商领域的新手卖家而言&#xff0c;他们往往困惑于如何在Wish上起步&#xff0c;因为该平台的运营模式与其他平台有所不同。Wish是一款基于手机端App的跨境电商平…

TypeScript 基础学习笔记:interface 与 type 的异同

TypeScript 学习笔记&#xff1a;interface 与 type 的异同 &#x1f3a3; 引言 在 TypeScript的世界里&#xff0c;精准的类型定义是保证代码质量与团队协作效率的关键。interface 和 type 作为两种核心的类型定义工具&#xff0c;它们各自承载着不同的设计意图与应用场景。本…

建材物料小程序商城的作用是什么

建材物料如门窗、马桶、涂料、瓷砖等有着大量需求者&#xff0c;传统模式中客户主要是同城进店咨询查看&#xff0c;但随时电商深入生活和商家模式更新&#xff0c;如今线上店铺消费也同样火热。 尤其是厂商或品牌经销商&#xff0c;无论线下还是线上都不影响生意开展&#xf…

C语言 | Leetcode C语言题解之第69题x的平方根

题目&#xff1a; 题解&#xff1a; int mySqrt(int x) {long int i 0;for(i0;;i){long int a i*i;long int b (i1)*(i1);if(a < x&&b > x){break;}}return i; }

LeetCode:三数之和

文章收录于LeetCode专栏 三数之和 给你一个包含n个整数的数组nums&#xff0c;判断nums中是否存在三个元素a、b、c &#xff0c;并使得a b c 0 &#xff1f;请你找出所有和为0且不重复的三元组。   注意&#xff1a;答案中不可以包含重复的三元组。   示例 1&#xff1a…

proxmox宿主机安装桌面

装完proxmox启动后一般进入shell界面&#xff0c;之后都是另外一台电脑连接web管理等操作&#xff0c;一直用起来还好。不过这样需要另外一台电脑连接管理操作&#xff0c;有时候调试时毕竟还是会有些不方便&#xff0c;就想能不能在宿主机上装个桌面做这类事&#xff0c;今天用…

Python基础学习之logging模块

在Python编程中&#xff0c;日志记录&#xff08;Logging&#xff09;是一个非常重要的功能。它不仅可以帮助我们追踪和调试代码中的错误&#xff0c;还可以记录程序运行时的关键信息&#xff0c;以便后续分析和优化。Python标准库中的logging模块为我们提供了强大的日志记录功…

第07-6章 应用层详解

HTTP、SSL&#xff1a;基于TCP&#xff0c;HTTP端口:80、HTTPS&#xff08;加密&#xff09;端口&#xff1a;443&#xff1b;FTP:基于TCP&#xff0c;两类端口&#xff1a;21、20&#xff08;数据传输之前需要建立连接此时是21&#xff0c;真正传输数据时用20&#xff09;TFTP…

Linux: Netfilter 简介

文章目录 1. 前言2. Netfilter 简介2.1 Netfilter 的功能2.2 Netfilter 示例2.3 Netfilter 实现概览2.3.1 Netfilter hook 的 注册 和 注销2.3.2 Netfilter hook 的触发2.3.2.1 NF_INET_PRE_ROUTING2.3.2.2 NF_INET_LOCAL_IN2.3.2.3 NF_INET_FORWARD2.3.2.4 NF_INET_LOCAL_OUT2…

【MySQL】——用户和权限管理(二)

&#x1f4bb;博主现有专栏&#xff1a; C51单片机&#xff08;STC89C516&#xff09;&#xff0c;c语言&#xff0c;c&#xff0c;离散数学&#xff0c;算法设计与分析&#xff0c;数据结构&#xff0c;Python&#xff0c;Java基础&#xff0c;MySQL&#xff0c;linux&#xf…

Day15-JavaWeb开发-Maven高级-分模块设计与开发继承与聚合私服

1. Maven高级-分模块设计与开发 2. Maven高级-继承与聚合 2.1 继承关系实现 2.2 版本锁定 2.3 聚合实现 3. Maven高级-私服 3.1 私服-介绍 3.2 私服-资源上传与下载 4. Web开发-完结

【mysql】深入探索mysql中的各种约束条件

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

快速的异地组网工具?

【天联】是一款能够快速搭建异地组网的工具&#xff0c;其应用场景非常广泛。 零售、收银软件应用&#xff1a;通过结合【天联】&#xff0c;医药、餐饮、商超等零售行业可以实现异地统一管理。不论是分布在不同地区的门店&#xff0c;还是总部和各个分支机构&#xff0c;都可以…

工业光源环形系列一平面无影光源特点

产品特点 ◆LED灯珠均匀排布经过漫射板特殊角度反射达到漫射效果&#xff1a; ◆光源均匀性高&#xff0c;漫射效果好。
最新文章