1. 引言
之前面试的次数也不多,投过后端开发和测开岗基本也就是问一下Python的装饰器和面向对象等一些简单的问题。今天面了某车企(岗位是Java后端开发),面试官超Nice,在看完我的博客后,问了<Python的线程安全>中的一个问题(当时也没想那么多),感觉给自己挖了一个大坑(print("赶紧填坑")
)
面试情况如下:
面试官:博客还不错,Python知识点整理的还不错((▽))
面试官:我不懂Python唉,现在的版本是2.0还是多少
我:现在都是Python3.9啦~之前的版本都太老啦~一些新特性都没有~经典类继承、新式类继承,还有钻石继承问题,深度和广度优先问题~巴拉巴拉~
面试官:我看你博客还写了多线程~
面试官:既然我不懂Python你不懂Java,那你从底层说说i++;
为什么不是线程安全的?
我:每个线程都有自己的工作内存,每个线程需要对共享变量操作时必须先把共享变量从主内存 load 到自己的工作内存,等完成对共享变量的操作时再 save 到主内存~巴拉巴拉
面试官:表情语言(你说的明显不是我想要的答案)
面试官:唉唉唉~下一个问题吧
2. 为什么不是i++;不是线程安全的?
2.1 如何实现线程安全
实现线程安全主要围绕线程私有资源和线程共享资源这两点,你需要识别出哪些是线程私有,哪些是共享的,这是最关键的:
- 不使用任何全局资源:只使用线程私有资源,这种通常被称为无状态代码;
- 线程局部存储:如果要使用全局资源,是否可以声明为线程局部存储,因为这种变量虽然是全局的,但每个线程都有一个属于自己的副本,对其修改不会影响到其它线程;
- 只读:如果必须使用全局资源,那么全局资源是否可以是只读的,多线程使用只读的全局资源不会有线程安全问题;
- 原子操作:原子操作是说其在执行过程中是不可能被其它线程打断的,像C中的
std::atomic
修饰过的变量,对这类变量的操作无需传统的加锁保护,因为C会确保在变量的修改过程中不会被打断。我们常说的各种无锁数据结构通常是在这类原子操作的基础上构建的 ; - 同步互斥:到这里也就确定了你必须要以某种形式使用全局资源,那么在这种情况下公共场所的秩序必须得到维护,那么怎么维护呢?那就只能通过同步或者互斥的方式了;
2.2 inc命令能否实现原子操作?
加一指令inc
inc a 相当于 add a,1 //i++
优点 速度比sub指令快,占用空间小
这条指令执行结果影响AF、OF、PF、SF、ZF标志位,但不影响CF进位标志位.
前面提到了原子操作,在C语言中我们使用i++;命令,底层调用的其实是inc命令,我们通过汇编查看一下底层调用吧:
通过felixcloutier_x86的网站,可以查看汇编-x86指令,我发现:inc 指令也是非原子操作,只有加LOCK
才能成为原子操作,而且inc不会修改CF寄存器,ADD会修改。
既然inc也无法保证线程安全,我们可以参考Linux内核中的atomic_inc
代码,如下图:
在Linux中的实现也是加上了LOCK
的。
原子操作需要硬件的支持,因此是架构相关的,其API和原子类型的定义都定义在内核源码树的
include/asm/atomic.h
文件中,它们都使用汇编语言实现,因为C语言并不能实现这样的操作。
2.3 实现原子操作
然而,翻了一圈之后我发现了gcc的的一个API定义了原子性相加的调用方法:
type __sync_fetch_and_add(type*ptr,type value,...);// m+n
type __sync_fetch_and_sub(type*ptr,type value,...);// m-n
type __sync_fetch_and_or(type*ptr,type value,...); // m|n
type __sync_fetch_and_and(type*ptr,type value,...);// m&n
type __sync_fetch_and_xor(type*ptr,type value,...);// m^n
type __sync_fetch_and_nand(type*ptr,type value,...);// (~m)&n
/* 对应的伪代码 */
{tmp=*ptr;*ptr op=value;returntmp;}
{tmp=*ptr;*ptr=(~tmp)&value;returntmp;} // nand
对比上面的i++;我们来实现看看:
通过汇编我们可以发现已经实现了原子操作,成功的上锁了。
3. 参考资料
关于更多LOCK
相关的参考如下:
Q.E.D.