原型变量、原子操作、原子性、内存序

一、原子变量、原子操作

  • 锁竞争:互斥锁条件变量、原子变量、信号量、读写锁、自旋锁。
  • 在高性能基础组件优化的时候,为了进一步提高并发性能,可以使用原子变量。
  • 性能:原子变量 > 自旋锁 > 互斥锁
    • 操作临界资源的时间较长时使用互斥锁(粒度大)
    • 如果只操作单个变量的话,可以使用原子变量(粒度小) 进行优化。
  • 给基础类型或者指针加上一个标记 std::atomic<T> 之后它就是原子变量对这些变量的操作就是原子操作
  • 原子变量是一种多线程编程中常用的同步机制,它能确保对共享变量的操作在执行时不会被其它线程的操作干扰,从而避免竞态条件。
  • 原子变量具备原子性,也就是要么全部完成,要么全部未完成。
  • 为什么会使用到原子变量 ?
    • 通常某一个变量对应的操作,其 CPU 的指令是大于 1 个指令的,在多线程环境下,可能会引发竞态,在这种线程不安全的情况下,我们需要为这个变量单独设置一个锁安全的标记 std::atomic<T>从而实现线程安全
    • 多线程环境下,确保对共享变量的操作在执行时不会被干扰,从而避免竞态条件。
std::atomic<T>
is_lock_free; // 是否支持无锁操作
store(T desired, std::memory_order order); // 用于将特定的值存储到原子对象中
load(std::memory_order order); // 用于获取原子变量的当前值// 访问和修改包含的值,将包含的值替换并返回它原来的值。如果替换成功,则返回原来的值
exchange(std::atomic<T>* obj, T desired);
/* 
比较一个值和一个期望值是否相等,如果相等则该值替换成一个新值,并返回 true,否则不做任何操作并返回 false。
*/
compare_exchange_weak(T& expected, T val, memory_order success, memory_order failure);
/*
compare_exchange_weak 函数是一个弱化版本的原子操作函数,因为在某些平台上它可能会失败并重试。
如果需要保证严格的原子性,应该使用 compare_exchange_strong 函数
*/
compare_exchange_strong((T& expected, T val, memory_order success, memory_order failure);fetch_add
fetch_sub
fetch_and
fetch_or
fetch_xor

二、原子性

  • 要么都做要么还没做,不会让其它核心看到执行的一个中间状态
  • 单处理器单核心实现原子性:
    • 只需要保证操作指令不被打断。
      • 屏蔽中断 → 不允许操作指令被打断,不让其它线程看到操作指令的中间状态
      • 底层硬件自旋锁 → 为了实现线程切换。
  • 多处理器或多核心实现原子性:
    • 除了要保证操作指令不被打断,还需要避免其它核心操作相关的内存空间
      • 以往的 0x86 lock 指令,锁总线避免所有内存的访问
      • 现在的 lock 指令,锁总线只阻止其它核心对相关内存空间的访问,也就是只锁住相关内存空间
      • 内存总线:CPU 需要通过内存总线访问内存。
      • 磁盘总线:CPU 需要通过磁盘总线访问磁盘。
  • 存储体系结构
    • 为什么要有 CPU 缓存:为了解决 CPU 运算速度与内存访问速度不匹配的问题,在 CPU 和内存之间设置了一些缓存。
      CPU 和磁盘之间也有一个缓存 → 高速缓冲区(page cache) → 为了解决 CPU 运算速度与磁盘访问速度不匹配的问题。
      
    • L1 和 L2 是核心独有的,L3 是核心共有的,也就是多个核心共用一个 L3
    • CPU 缓存的基本单位:cache line,64 字节(64 B)。每次最少读取的数据空间(不管用户要读的数据空间有多小)。
      • flag:标识缓存中的数据是否可用,存储的是缓存的状态值(MESI)。
      • tag:索引数据是否在缓存中、数据在缓存中的位置。
      • data:具体存储的数据。

在这里插入图片描述

  • 在 CPU 缓存的基础上,CPU 如何读写数据 ? 注意:CPU 缓存中的数据永远比内存中的数据要新
    • 写直达策略
      • 每次写操作既会写到 CPU 缓存中,也会写到内存中,写性能会很低。
    • 写回策略(write-back)(现代 CPU 使用的策略)
      • 尽量避免每次写数据都把数据写到内存中
      • 写操作
        • 先检查是否命中 CPU 缓存,如果命中了就直接写,并标记为脏数据(CPU 缓存中的数据和内存中的数据不一致)
        • 如果没有命中,就需要在 CPU 缓存中找一块区域来存储新数据,也就是定位缓存块
          • 如果有可用的缓存块(还没有存储其它数据的缓存块) 就直接使用。
          • 如果没有可用的缓存块了,则采用 LRU 策略去定位缓存块
            • 该缓存块存储的数据是脏数据,先把这个缓存块中的数据刷到内存中,然后用这个缓存块去存储新数据,并标记为脏数据
            • 该缓存块存储的数据不是脏数据,直接将新数据写入到这个缓存块中,并标记为脏数据
      • 读操作
        • 先检查是否命中 CPU 缓存,如果命中了就直接返回。
        • 如果没有命中,从内存中读取数据并返回,然后定位缓存块去存储该数据。
          • 如果有可用的缓存块就直接使用。
          • 如果没有可用的缓存块了,则采用 LRU 策略去定位缓存块。
            • 该缓存块存储的数据是脏数据,先把这个缓存块中的数据刷到内存中,然后用这个缓存块去存储刚刚从内存中读取的数据,并标记为非脏数据
            • 该缓存块存储的数据不是脏数据,直接将刚刚从内存中读取的数据写入到这个缓存块中,并标记为非脏数据
  • 为什么会有缓存一致性问题 ?
    • CPU 是多核心的。
    • 基于写回策略将会出现缓存不一致的问题。
      • 数据在核心 0 的 CPU 缓存中,但是还没写到内存中;核心 2 的 CPU 缓存中没有该数据,于是会从内存中读取该数据,但是核心 2 从内存中读到的数据与核心 0 的 CPU 缓存中的数据不一致。
  • 如何解决缓存不一致的问题 ?
    • 写传播:总线嗅探(bus snooping)
      • 监听发布者模式:每一个核心都会监听总线上的写事件,当某个核心写数据的时候,会基于总线进行广播,其它的核心读到了这个事件之后,会自动修改自己的数据。
    • 事务的串行化:锁 + lock 指令
      • 多个核心对同一个缓存块进行读写操作的时候,必须要串行执行,否则会带来不确定性。
      • core0 写 i = 10,锁总线,必须等 i = 10 写操作结束后,core1 才能写 i = 20,这样 core2 读到的 i 就一定等于 20。

在这里插入图片描述

  • 优化:尽量减小写传播给总线带来的带宽压力
    • 两个策略:
      • 写传播:如何减少无效的监听,减小总线带宽的压力。
      • 串行化机制:如何锁总线。
  • MESI 一致性协议
    • 基于总线嗅探机制实现了事务串行化,通过状态机降低总线带宽的压力
    • 4 个状态
      • Modified:已修改,某个数据块已修改但是没有同步到内存中。
      • Exclusive:独占,某个数据块只在某核心的缓存中,并且此时缓存和内存中的数据是一致的。
      • Shared:共享,某个数据块在多个核心的缓存中,并且此时缓存和内存中的数据是一致的。
      • Invalidated:已失效,某个数据块在核心中已失效,不是最新的数据。
        • core0 中的数据:i = 5,core1 中的数据:i = 5,内存中的数据:i = 5;当 core0 写 i = 10 时,会通过总线嗅探,将 core1 中的数据 i = 5 的状态修改为 Invalidated。
    • 锁住 M 和 E 状态(因为 M 和 E 状态是不需要广播的),避免相关内存的访问

三、内存序

  • 原子性还没有解决避免竞态条件的问题
  • 为什么会有内存序问题
    • 编译器优化重排
      • C++ 在编译代码的时候,为了提高未来运行的效率,编译器会对代码指令进行编译优化重排。
    • CPU 指令优化重排
      • CPU 在运行的时候,也会对指令进行优化重排:在实现原子性的时候,会锁 M、E 状态,既然核心不能操作相关的内存区域,那就去操作不相关的内存区域,这样核心就不会干等着,从而提高整体的运行效率。
    • 在 CPU 看来,i 和 j 没有任何关系,可以并行处理。但程序的逻辑是:在 j += 2 的时候,i 已经 += 1了,在多线程条件下,就会出现竞态问题。
      int i = 0;
      int j = 0;
      i += 1;
      j += 2;
      
  • 内存序规定了什么
    • 规定了多个线程访问同一个内存地址时的语义
      • 同步性某个线程对内存地址的更新何时能被其它线程看见
      • 顺序性某个线程对内存地址访问附近可以做怎么样的优化
  • 内存模型
    • 这里所指的内存模型对应缓存一致性模型,作用是对同一时间的读写操作进行排序,在不同的 CPU 架构上,这些模型的具体实现方式可能不同,但是 C++ 11 屏蔽了内部细节,不用考虑内存屏障。可能有时使用的模型粒度比较大,会损耗性能,当然还是使用各平台底层的内存屏障粒度更准确,效率也会更高。
    • memory_order_relaxed:松散内存序。
      • 只用来保证对原子对象的操作是原子的,在不需要保证顺序时使用。
      • 读操作和写操作都可以使用。
      • 效率最高。
      // 没有同步性,其它线程可能读到的不是最新的值
      // 不干预 编译器或 CPU 的优化
      s.load(std::memory_order_relaxed);
      
      在这里插入图片描述
    • memory_order_release:释放操作。
      • 在写入某原子对象时,当前线程的任何前面的读写操作都不允许重排到这个操作的后面去并且保证其它线程可以读取到该原子对象的最新值
      • 通常与 memory_order_acquire 配对使用。
      • 只能在写操作中使用。
      • 依据前面的才写入
      // 具备同步性,其它线程读到的是最新的值
      // 干预了优化
      s.store(10, std::memory_order_release);
      
      在这里插入图片描述
    • memory_order_acquire:获取操作。
      • 在读取某原子对象时,当前线程的任何后面的读写操作都不允许重排到这个操作的前面去并且保证当前线程可以读取到该原子对象的最新值
      • 只能在读操作中使用。
      • 后面的依据读取的
      // 具备同步性,当前线程读到的是最新的值
      // 干预了优化
      s.load(std::memory_order_acquire);
      
      在这里插入图片描述
    • memory_order_acq_rel:获得释放操作。
      • 一个读 — 修改 — 写操作,同时具有获得语义和释放语义,即它前后的任何读写操作都不允许重排,并且保证其它线程可以读取到该原子对象的最新值、当前线程可以读取到该原子对象的最新值。
    • memory_order_seq_cst:顺序一致性语义。
      • 对于读操作相当于获得,对于写操作相当于释放,对于读 — 修改 — 写操作相当于获得释放。
      • 是所有原子操作的默认内存序,并且会对所有使用此模型的原子操作建立一个全局顺序,保证了多个原子变量的操作在所有线程里观察到的操作顺序相同。
      • 效率最低。
#include <atomic>
#include <thread>
#include <assert.h>
#include <iostream>// g++ relaxed.cc -o relaxed -lpthreadstd::atomic<bool> x, y;
std::atomic<int> z;void write_x_then_y()
{x.store(true,std::memory_order_relaxed);  // 1y.store(true,std::memory_order_relaxed);  // 2
}void read_y_then_x()
{while(!y.load(std::memory_order_relaxed));  // 3if(x.load(std::memory_order_relaxed))  // 4++z;
}
// z 会不会等于 1 ?  不一定int main()
{for (int i = 0; i < 100000; i++) {x = false;y = false;z = 0;std::thread b(read_y_then_x);std::thread a(write_x_then_y);b.join();a.join();int v = z.load(std::memory_order_relaxed);if (v != 1)std::cout << v << std::endl;}return 0;
}
#include <atomic>
#include <thread>
#include <assert.h>
#include <iostream>// g++ acquire_release.cc -o acquire_release -lpthreadstd::atomic<bool> x,y;
std::atomic<int> z;void write_x_then_y()
{x.store(true,std::memory_order_relaxed);  // 1 y.store(true,std::memory_order_release);  // 2   y = true x= true
}void read_y_then_x()
{while(!y.load(std::memory_order_acquire));  // 3 自旋,等待 y 被设置为trueif(x.load(std::memory_order_relaxed))  // 4++z;
}
// z 能确保读到 1int main()
{x = false;y = false;z = 0;std::thread a(write_x_then_y);std::thread b(read_y_then_x);a.join();b.join();std::cout << z.load(std::memory_order_relaxed) << std::endl;return 0;
}

四、互斥锁

  • 如何实现互斥锁:
    • 互斥锁首先需要做一个内存标记记录的是线程 ID,因为互斥锁需要进行线程切换。
    • 互斥锁是为了保护临界资源,同时只允许一个线程去访问临界资源,所以会有一个阻塞队列,通过阻塞队列唤醒其它线程去访问临界资源。
    • 要屏蔽中断。
    • 底层硬件自旋锁。
  • 互斥锁的表现:
    • 先在用户态自旋一会儿。
    • 获取失败,把任务挂起(放到阻塞队列中),核心会切换其它线程去执行。
    • 休眠一段时间再次尝试获取锁。

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【leetcode】 c++ 数字全排列, test ok

1. 问题 2. 思路 3. 代码实现 #if 0 class Solution { private:vector<int> path; // 满足条件的一个结果 vector<vector<int>> res; // 结果集 void backtracking(vector<int> nums, vector<bool> used){// 若path的个数和nums个数相等&…

K8S基于containerd做容器从harbor拉取镜

实现创建pod时&#xff0c;通过指定harbor仓库里的镜像来运行pod 检查&#xff1a;K8S是不是用containerd做容器运行时&#xff0c;以及containerd的版本是不是小于1.6.22 kubectl get nodes -owide1、如果containerd小于 1.6.22&#xff0c;需要先升级containerd 先卸载旧的…

金三银四面试题(十六):MySQL面试都问什么(1)

在开发岗位面试中&#xff0c;MySQL基本是必考环节。所以接下来我们就进入MySQL八股文环节&#xff0c;看看都有哪些高频考题。 MySQL 中有哪些不同的表格&#xff1f; 在MySQL中&#xff0c;可以创建多种不同类型的表格&#xff0c;其中一些常见的类型包括&#xff1a; InnoD…

【边缘智能】00_边缘计算发展背景

本系列是个人学习《边缘就算基础知识入门》的笔记&#xff0c;仅为个人学习记录&#xff0c;欢迎交流&#xff0c;感谢批评指正 移动物联设备产生海量数据&#xff0c;数据密集型移动智能应用&#xff0c;计算密集、动态性高&#xff0c;实时性强 传统云计算架构 基于广域互联…

不开玩笑,你应该像「搬砖」一样写代码!斯坦福大学研究如是说

由于程序员不可避免要进行很多重复性的工作&#xff0c;并且工作强度很高&#xff0c;导致有一种自嘲的说法出现&#xff1a;程序员们自称自己每天都在搬砖&#xff08;实际上很多职场人都这么自嘲&#xff09;。我相信当我们说工作像「搬砖」的时候&#xff0c;只是在表达一种…

谷歌DeepMind发布Gecko:专攻检索,与大7倍模型相抗衡

ChatGPT狂飙160天&#xff0c;世界已经不是之前的样子。 新建了免费的人工智能中文站https://ai.weoknow.com 新建了收费的人工智能中文站https://ai.hzytsoft.cn/ 更多资源欢迎关注 Gecko 是一种通用的文本嵌入模型&#xff0c;可用于训练包括文档检索、语义相似度和分类等各…

2-django、http、web框架、django及django请求生命周期、路由控制、视图层

1 http 2 web框架 3 django 3.1 django请求生命周期 4 路由控制 5 视图层 1 http #1 http 是什么 #2 http特点 #3 请求协议详情-请求首行---》请求方式&#xff0c;请求地址&#xff0c;请求协议版本-请求头---》key:value形式-referer&#xff1a;上一次访问的地址-user-agen…

一开始我只是接单试试水而已,后来我居然财富自由了!

一开始我只是抱着试一试的心态&#xff0c;浅浅的尝试了一下网上接单&#xff0c;没办法&#xff0c;这风太大了&#xff01;网上个个儿说的神乎其神的&#xff0c;尤其是动不动就几十W&#xff0c;没办法&#xff0c;我眼红啦&#xff01;赚钱嘛&#xff0c;不丢人&#xff01…

【JavaWeb】百度地图API SDK导入

百度地图开放平台 | 百度地图API SDK | 地图开发 (baidu.com) 登录注册&#xff0c;创建应用&#xff0c;获取AK 地理编码 | 百度地图API SDK (baidu.com) 需要的接口一&#xff1a;获取店铺/用户 所在地址的经纬度坐标 轻量级路线规划 | 百度地图API SDK (baidu.com) 需要的…

图片批量高效管理,图片像素缩放支持自定义操作,让图像处理更轻松

在数字化时代&#xff0c;图片管理成为了我们生活和工作中不可或缺的一部分。无论是个人用户还是企业用户&#xff0c;都需要对大量的图片进行有效的管理和处理。然而&#xff0c;面对众多的图片&#xff0c;如何进行批量管理并对其进行像素缩放成为了一个挑战&#xff0c;该如…

【Linux】指令

1. 简单指令 whoami 显示当前登入账号名 ls /home 现在有的用户名 adduser 用户名 新加用户&#xff08;必须在root目录下&#xff09; passwd 用户名 给这个用户设置密码 userdel -r 用户名 删除这个用户 pwd 显示当前所处路径 stat 文件名 / 文件夹名 显示文件状…

基于JAVA+SSM+微信小程序+MySql的图书捐赠管理系统设计与实现(前后端分类)

一、项目背景介绍&#xff1a; 在当今社会&#xff0c;图书捐赠是一种普遍而有益的行为&#xff0c;旨在促进阅读、教育和知识传播。图书捐赠可以帮助改善教育资源不足的地区、学校和社区的阅读环境&#xff0c;提供更多的学习机会和知识获取途径。随着互联网和移动技术的发展&…