多线程访问基本数据类型需要加锁吗?

一、问题

最近跟同事一起讨论多线程读写数据需要加锁的事,他问了个问题,多线程读写基本数据类型也需要加锁吗?只是设置一个指针,是不是只会出现读到旧值的问题,而不会崩溃?

要回答这个问题,其实是要看往内存写基本数据类型是不是原子的,也即另一线程会不会看到设置到一半的数据。这就需要我们详细分析一下写内存时究竟发生了哪些事,及其对应的不加锁时可能出现的多线程风险。

二、流程分析

代码中给一个变量设置新值,首先会由编译器将这行代码转成汇编指令(STORE),运行时CPU执行这些访存指令,将数据设置给相应的内存地址。

1)编译器规范

所以第一步是编译器对这种情况的处理:

  • C++标准中规定:多线程访问同一个非atomic对象,且至少有一个线程是写操作,是未定义行为。

所以从规范角度看,多线程场景只要有写操作,必须加锁(或使用原子变量)才能保证安全性。

如果未加锁,编译器有可能会将一个变量写操作转成多条指令(如将64位的数据操作转成两个32位的store指令),如果是转成两条指令,那么多线程情况下就可能会出现另一线程只看到更改了一半数据的问题。

2)访存及其多线程风险

接下来是CPU实际执行内存访问,下面拆解一下其具体流程:

CPU访问内存(虚拟地址)的步骤:

  1. 虚拟地址转物理地址(可与2.1并行)
  • 1.1 查询TLB:虚拟地址中除了页内地址的部分,直接查询TLB看是否缓存有其物理地址
    • 如果有,则直接返回物理地址
    • 如果没有,则需要查页表:页表存在内存中,所以查页表意味着增加了一次虚拟地址的访问
  1. 查L1 cache(VIPT:Virtually Indexed Physically Tagged)
  • 2.1 将虚拟地址中的一部分(页内偏移的高位)作为index找到cache line组
    • 假设cache line大小为64byte,cache大小为32K,组相联路数为8,则cache line组数 = 32K/(64 * 8) = 64组
    • cacheline 64byte,占地址位数的6位:[0,5]
    • 组数64组,占地址位数6位:[6,11]
    • 如果页大小为4K,则页面偏移一共12位,则地址位数中的[0,11]都是页内偏移,也即虚拟地址与物理地址的[0,11]位是相同的,所以可以用虚拟地址的[6,11]位来直接查找cache组的index,而不用等到步骤1.返回物理地址再开始查找
  • 2.2 拿物理地址(步骤1.)中的高位与上一步找到的组中各cache line中的tag进行比较,找到对应的cache line
    • 如果存在,则直接返回cache line数据给CPU Core
    • 如果不存在,则说明cache中无缓存,需要等待下级缓存或memory返回数据
  1. 查L2 cache(PIPT:Physiccally Indexed Physiccally Tagged)
  • 3.1 物理地址的一部分作为index找到cache line组
  • 3.2 物理地址中的高位与组中各cache line的tag进行比较,找到相应的cache line
    • 如果存在,则返回cache line数据
    • 如果不存在,则查询memory
  1. 查物理内存
  • 4.1 CPU将物理地址发送到FSB(Front Side Bus),通过北桥发送给内存控制器,得到内存数据
  • 4.2 填充各级cache
  • 4.3 返回数据给CPU Core
  1. 在写回(WriteBack)模式下,新写入的数据会被更新到当前CPU core的L1 cache line中,而不直接写进内存,后续必要时再将cache line写入内存。

访存过程中的多线程风险:

因为cache中数据更新的粒度是cache line,如果被访问数据跨越了多个cache line,则可能导致另一个线程看到更新了一半的数据

  • 基本类型数据:如果是默认对齐的情况(没有编译期指定alignment或pack),编译器能保证它不会跨越cache line,不跨cache line的操作是原子的
  • 非基本类型数据(如struct),有可能出现一半字段在cache line 1,一半在cache line 2的情况,如果业务逻辑中依赖了struct中两个字段要一致,则可能出现预期外数据

参考资料

相关推荐
小飞猪Jay1 小时前
C++面试速通宝典——13
jvm·c++·面试
rjszcb2 小时前
一文说完c++全部基础知识,IO流(二)
c++
韩楚风3 小时前
【linux 多进程并发】linux进程状态与生命周期各阶段转换,进程状态查看分析,助力高性能优化
linux·服务器·性能优化·架构·gnu
小字节,大梦想3 小时前
【C++】二叉搜索树
数据结构·c++
吾名招财3 小时前
yolov5-7.0模型DNN加载函数及参数详解(重要)
c++·人工智能·yolo·dnn
我是哈哈hh3 小时前
专题十_穷举vs暴搜vs深搜vs回溯vs剪枝_二叉树的深度优先搜索_算法专题详细总结
服务器·数据结构·c++·算法·机器学习·深度优先·剪枝
憧憬成为原神糕手3 小时前
c++_ 多态
开发语言·c++
郭二哈3 小时前
C++——模板进阶、继承
java·服务器·c++
挥剑决浮云 -4 小时前
Linux 之 安装软件、GCC编译器、Linux 操作系统基础
linux·服务器·c语言·c++·经验分享·笔记
丶Darling.4 小时前
LeetCode Hot100 | Day1 | 二叉树:二叉树的直径
数据结构·c++·学习·算法·leetcode·二叉树