理解原子操作与CAS锁

简介: 理解原子操作与CAS锁

线程间内存访问同步的问题

先看一小段程序

#include <chrono>
#include <iostream>
#include <thread>

#define USE_ATOMIC 1
const int test_times = 100;

#if USE_ATOMIC
    #include <atomic>
    std::atomic<int> count{0};
#else
    int count = 0;
#endif

void increase(int num) {
    for (int i = 0; i < num; i++) {
    #if USE_ATOMIC
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
        count.fetch_add(1);
    #else
        ++count;
    #endif
    }
}

int main() {

    for(int i = 0; i < test_times ; i++) {
    #if USE_ATOMIC
        count.store(0);
    #else
        count = 0;
    #endif
        std::thread t1(increase, 500);
        std::thread t2(increase, 500);
        std::thread t3(increase, 500);
        std::thread t4(increase, 500);
        t1.join();
        t2.join();
        t3.join();
        t4.join();
    #if USE_ATOMIC
        std::cout << "count.load() =  " << count.load() << std::endl;
        if (count.load() != 2000) {
    #else
        if (count != 2000) {
    #endif
            std::cout << "i: " << i << " count :" << count << std::endl;
            break;
        }

    }
    return 0;
}

USE_ATOMIC 1 程序运行完后,直接退出:说明每次创建的t1,t2,t3,t4四个线程都能把原子变量std::atomic count,增加到2000,程序执行100次,一次失误都没有
USE_ATOMIC 0 程序输出 i: 0 count :1298:说明没有使用原子变量的情况下,i的值是0说明程序只跑了一次,就出了问题,count并没有通过t1,t2,t3,t4四个线程增加到2000。

通过这个例子只想说明,在多线程的程序,对变量的访问存在内存同步的问题(t1,t2,t3,t4四个线程对普通变量 int count 的修改,并没有很好的同步给彼此)。

怎么理解这种现象呢?---- 需要理解多核cpu的存储体系结构。

理解cpu的存储体系结构

cpu存储架构

cpu的速度非常快,内存较慢,为了解决cpu与内存之间速度不匹配的问题,在cpu 与内存之前加了很多级缓存。
类似于写磁盘的操作,写之前也会先写到cache buffer,然后再由cache buffer写入磁盘。
在这里插入图片描述
上图是一个双核的cpu
锁读内存相关的总线: cache line (缓存行,大小为64字节,在不同架构和系统中可能会有所变化)
cache L1,L2 是cpu每个核心独享的, L3是所有核心共享的cache
cpu访问内存的顺序如下:
L1---> L2 ---> L3 ----> 主存(内存)
先在L1中找,没有找到,去L2找,没有找到去L3找,没有找到去主存找,读到数据之后,
又会反过来写入到缓冲中去,等一下次再需要读数据的时候,就可以直接从缓存中取了。
但是缓存的都是比较小的,里面会不停的有数据的进入与淘汰,谁负责这些呢?MESI协议,LRU策略等。

cache line

在这里插入图片描述
cache line, cpu从缓存中读数据的基本单元
flag| tag | data: flag判断缓存是否失效 (存MESI 的状态);tag:数据存在哪个地方; data是数据48byte

了解一下写回策略 write-back

cpu要写一个数据,先根据tag, 判断缓存是否命中

1 命中直接写,标记脏数据(直接写缓存)
2 没有命中,缓存里没有值:先在缓存中定位一个缓存块cache line
    a 脏数据(数据还没有写到内存里), 把数据写回内存(通过LRU策略淘汰掉的数据才写回内存)
    b ==数据尽量停留在缓存里==,数据在缓存里被淘汰了,才写回内存
    c  内存与缓存里的数据一样,不标记脏数据

==数据尽量停留在缓存里== 这是核心思想,只有这样cpu取数据、指令会快一些

多线程运行在cpu的多核之中,数据怎么共享,怎么同步?

通过事件串行化

假如有t1,t2两个线程对i进行写,t3对i进行读,t1,t2写完的顺序不一样,就会导致t3读到数据可能与最终的结果不一致。
这时可以通过锁指令,分别对t1,t2上锁,确保t3读到正确的值。
但是如果每一次,都广播给其它的核心,代价较大,有可能浪费带宽(不是所有的核心都需要i),这里引入MESI来解决问题。

通过MESI

MESI是一种缓存一致性协议,用于在多处理器系统中保持数据的一致性。它定义了四种状态:Modified(修改),Exclusive(独占),Shared(共享)和Invalid(无效)。当一个处理器访问某个内存位置时,它将读取该位置的值,并将其保存在缓存中。此时,该缓存行的状态被标记为“独占”,表示该处理器是唯一拥有该数据的。如果另一个处理器需要访问相同的内存位置,则必须通过总线请求缓存行。如果该缓存行的状态为“共享”,则该处理器可以直接从缓存中读取数据,而不需要访问主内存。如果该缓存行被标记为“修改”状态,则表示当前处理器已经对其进行了修改,并且需要向其他处理器发送通知,以便它们更新自己的副本或者使其失效。

使用MESI协议可以确保多个处理器之间共享数据时,各自拥有最新版本,并且避免了数据冲突和不一致性问题。

原子操作

1 对于单处理,单核心的机器,一个操作(三条指令)要保持它的原子性,就是确保这三条指令执行的时候不被打断就行。
在执行这三条指令之前屏蔽掉中断,执行完,恢复中断即可。

2 对于多处理器多核心的机器(不同处理间有高速互联总线HSIB ),确保1的同时还需要做到:

以往的处理方式是锁住HSIB,确保原子操作的时候,两个处理器之前不交换数据
现在是通过lock指令,阻止其他核心对相关内存空间的访问

CAS锁

CAS锁(Compare-and-Swap Lock)是一种乐观锁,也被称为无阻塞算法。它基于CPU提供的原子指令实现,用于解决并发环境下对共享资源的竞争访问问题。

CAS锁操作包括三个参数:内存位置V、期望值A和新值B。当V中的值等于A时,将V中的值修改为B,并返回true;否则不修改V中的值,并返回false。通过多次调用CAS操作,可以实现并发环境下对某个共享变量进行安全地读写。

使用CAS锁需要注意以下几点:

1 CAS操作是原子性的,但不能保证在高并发环境下绝对安全;
2 在使用CAS锁时,需要确保所有线程都使用相同的期望值A;
3 如果期望值A与当前内存位置V中的值不匹配,则需要重新尝试操作直到成功。

常用到的方法

            compare_exchange_strong  ------------  会阻塞cpu, 会慢一些
          compare_exchange_weak   ---------------  有可能失败,性能高,  可以加while直到它成功

文章参考与<零声教育>的C/C++linux服务期高级架构系统教程学习:链接

相关文章
|
8月前
|
人工智能 数据可视化 安全
2025年销售自动化工具选型指南
本文探讨了企业在数字经济时代选择合适CRM系统的重要性,分析了选型的5大核心维度:AI能力、全流程闭环管理、生态集成能力、数据安全合规及供应商综合实力。同时对比了5家国内外CRM厂商,如Salesforce、纷享销客、Hubspot等,并指出企业常遇的3大选型误区,包括功能冗余、忽视数据迁移成本和迷信行业模板。文章强调,适合企业的CRM需与战略、管理和业务流程深度适配,而非单纯追求技术堆砌。最终提醒管理者,选择CRM时应以实际需求为导向,而非盲目迷信国际品牌。
|
11月前
|
运维 监控 算法
解锁三维视频融合:重塑视觉体验与行业应用新格局
三维视频融合,解锁视觉新境界!实时视频嵌入三维空间,城市监控如临现场,工业运维精准高效,教育体验仿若亲为。跨越行业壁垒,革新视觉呈现!
315 8
|
Ubuntu Oracle 关系型数据库
Oracle VM VirtualBox之Ubuntu 22.04LTS双网卡网络模式配置
这篇文章是关于如何在Oracle VM VirtualBox中配置Ubuntu 22.04LTS虚拟机双网卡网络模式的详细指南,包括VirtualBox网络概述、双网卡网络模式的配置步骤以及Ubuntu系统网络配置。
1592 3
|
索引
filebeat 设置索引的 max_result_window
在 Filebeat 中设置索引的 max_result_window 需要修改 Elasticsearch 的索引模板。max_result_window 参数定义了在 Elasticsearch 中执行搜索时,最大返回文档的数量。默认情况下,该值为 10000。 要修改该值,可以按照以下步骤操作: 打开 Filebeat 的配置文件。 找到输出部分,其中定义了 Elasticsearch 输出。 在 Elasticsearch 输出配置中,找到索引模板相关的配置。 确保你已经定义了自定义的索引模板(如果没有,请创建一个)。 在索引模板中,设置 max_result_window 参数为
253 1
|
监控 算法 Linux
Linux下工具tc详细讲解及限制IP和端口实例
TC (Traffic Control) 是Linux内核中提供的一个用于控制和管理网络流量的强大工具,它允许用户实现QoS(Quality of Service)策略,包括带宽限制、优先级控制、延迟保证等。TC基于内核的队列 discipline (qdisc) 和流量类别(class) 体系结构,允许对进入或离开网络接口的数据流进行复杂的整形和过滤。
1118 0
|
存储 Java 数据库
基于SpringBoot的CSGO赛事管理系统(程序+数据库+文档)
基于SpringBoot的CSGO赛事管理系统(程序+数据库+文档)
|
JavaScript 前端开发
pdf.js插件使用记录,在线打开pdf
原文:pdf.js插件使用记录,在线打开pdf 天记录一个js库:pdf.js。主要是实现在线打开pdf功能。因为项目需求需要能在线查看pdf文档,所以就研究了一下这个控件。 有些人很好奇,在线打开pdf文档浏览器不是支持吗。
2931 1
|
SQL 缓存 负载均衡
Web Security 之 HTTP Host header attacks(上)
Web Security 之 HTTP Host header attacks
1016 0
|
Web App开发 存储 缓存
ptmalloc、tcmalloc与jemalloc对比分析(三)
ptmalloc、tcmalloc与jemalloc对比分析(三)
2069 0
|
Python 容器
Python Qt GUI设计:QMdiArea和QMdiSubWindow类实现多文档界面(拓展篇—3)
一个典型的GUI应用程序可能有多个窗口,选项卡控件和堆栈窗口控件允许一次使用其中的一个窗口。然而,很多时候这种方法不是很有用,因为其他窗口的视图是隐藏的一种同时显示多个窗口的方法是,创建多个独立的窗口,这些独立的窗口被称为SDI(Single Document Interface,单文档界面),每个窗口都可以有自己的菜单系统、工具栏等。这需要占用较多的内存资源。

热门文章

最新文章