从单核CPU系统角度看并发问题

  • Post author:
  • Post category:其他



1,问题引入:

在单核cpu系统中;进程有个全局量 int g_i = 0,在进程中开10个线程,每个线程都不对 g_i 加锁的情况下做1亿次自增操作 (g_i++) ;主线程等待所有的线程结束后,再打印 g_i 的值能保证是 10 亿吗?


2,问题初步思考:

先考虑在多核cpu系统中,无锁情况下,两个cpu可能同时读取 g_i 到

各自cpu寄存器

,同时在各自寄存器中自增,然后写相同的值到内存;显然这里的自增操作有两次,实际值只是自增了一次,因此不能保证最终的g_i值是10亿。

再考虑单核cpu系统,因为一个cpu只能运行一个进程(从linux内核角度线程也当做进程处理,这里不影响线程的讨论),那么问题中的10个线程必然是在单核cpu中交替调度运行的。单核cpu的

并发

可以理解为“伪并发”,而多核cpu系统中不同cpu的

并行

才是“真并发”。那么单核cpu线程调度会导致共享资源的不一致吗?或者是如何导致共享资源不一致的?


3,代码调试演示:

先整个单核cpu系统环境(vmware设置linux系统cpu核数为1),


3.1 先看不加锁情况

,代码运行的结果;在到达稳态后,g_i 的值仅有 2 亿多.

图1


3.2 再看加锁情况

,代码运行的结果;在到达稳态后,g_i 的值正好是10亿.

图2

为什么在单核cpu系统,不加锁会得出错误的结果?


4,结果分析


4.1 反汇编

将无锁版的可执行程序反汇编后,如下图所示。从汇编中也可以看到一些规则,比如未经初始化的全局变量,放在虚拟进程空间的 bss 区域. 子线程可以直接访问主进程的 bss 区域,

实际上


子线程能访问主进程的整个用户虚拟地址空间

。包括:代码区域,data区域,bss区域,堆,所有共享库代码。此外,主进程栈空间能被子线程访问吗?实际上也可以通过指针参数传递给子线程来访问。

图3


4.2 分析汇编

一个重要的概念是:一组运行在一个进程的上下文中的每个并发线程,都有其独立的

线程上下文

: 包括 线程ID,栈,栈指针,PC(%rip),条件码,通用目的寄存器(16个,%r[a-d]x,… %rbp,%rsp,%r8-15)。结合下图对@1,@2,@3 步骤逐个分析。

@1:线程1执行 mov 0x200942(%rip), %eax  指令,0x200942(%rip) 是基址+偏移量寻址,因为 %rip (PC) 表示将执行的下一条指令的地址,结合图3,%rip 值是0x400714 的下一个指令地址 0x40071a,因此 0x200942(%rip) 表示的地址是 M[0x200942+0x40071a] = M[0x60105c];正好是 图3  的 bss 区域的 g_i 地址。该指令将 g_i 的值传送到 %eax 寄存器。

@2:线程1执行完 mov 0x200942(%rip), %eax  指令后,被线程2抢占了。此时需要线程切换,

线程切换内核需要保存被换出的线程的上下文

(线程ID,栈,栈指针,PC(%rip),条件码,通用目的寄存器(其中就有%[r/e]ax))。一个问题是,如果此时线程1的%eax值是5000,那么经过@2、@3步骤切换回线程1,线程1在执行 add $0x1, %eax 之前,%eax的值是5000还是5001? 这个小问题是整个大问题的关键。

@3:线程1被恢复执行,在恢复执行前,

内核需要恢复

线程1的

线程上下文

,其中就包括 %eax 寄存器的值。如果@2被抢占步骤线程1的%eax是5000,那么此时被恢复的%eax应该还是 5000 (这个和 线程2 的%eax由操作系统层面保证区分开来)。

通过 @1,@2,@3 步骤的分析,以 0x200942(%rip) = g_i = 5000 为例,可以发现 线程1 被抢占后继续运行的结果是 5001,线程2 运行的结果同样也是 5001,虽然两个线程一共对 g_i 操作了两次自增,实际 g_i 的值仅自增了一次。由此得出的结论是:不管单核的“伪并发”还是多核的“真并发”,不加保护的操作共享资源都可能会导致资源不一致。



版权声明:本文为sf_jiang原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。