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

一、问题

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

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

二、流程分析

代码中给一个变量设置新值,首先会由编译器将这行代码转成汇编指令(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中两个字段要一致,则可能出现预期外数据

参考资料

相关推荐
张太行_1 小时前
C++中的析构器(Destructor)(也称为析构函数)
开发语言·c++
涛ing5 小时前
32. C 语言 安全函数( _s 尾缀)
linux·c语言·c++·vscode·算法·安全·vim
独正己身6 小时前
代码随想录day4
数据结构·c++·算法
我不是代码教父9 小时前
[原创](Modern C++)现代C++的关键性概念: 流格式化
c++·字符串格式化·流格式化·cout格式化
利刃大大9 小时前
【回溯+剪枝】找出所有子集的异或总和再求和 && 全排列Ⅱ
c++·算法·深度优先·剪枝
子燕若水9 小时前
mac 手工安装OpenSSL 3.4.0
c++
*TQK*9 小时前
ZZNUOJ(C/C++)基础练习1041——1050(详解版)
c语言·c++·编程知识点
ElseWhereR10 小时前
C++ 写一个简单的加减法计算器
开发语言·c++·算法
*TQK*10 小时前
ZZNUOJ(C/C++)基础练习1031——1040(详解版)
c语言·c++·编程知识点
※DX3906※11 小时前
cpp实战项目—string类的模拟实现
开发语言·c++