更多精彩内容,欢迎关注作者微信公众号:码工笔记

一、问题

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

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

二、流程分析

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

参考资料