线程这个概念早在多核CPU出现之前就提出来了,单核时代的多线程主要是为了让CPU尽量不处于空闲状态,使其计算能力始终能得到利用。但本质上讲,在任意时刻只有一个线程在执行。
尽管任意时刻只有一个线程在执行,但是依然有些问题需要解决,其中最重要的就是线程安全。这个问题的来源很简单,我之前说过,CPU对指令是一条一条执行的,不过要注意的是,高级语言中的一行代码在汇编语言层面上,可能由多条汇编指令组成的。举一个简单的例子,在c语言中对某个变量做自增操作:
int a = 0;
int add(){a += 1;return a;
}
不过变成汇编指令就不这么简单了,其中实现自增的主要是这两条指令(大部分指令省略):
movl a(%rip), %eaxaddl $1, %eax
这里之所以在例子中使用全局变量,是因为线程安全和数据同步主要是针对全局变量这类可以共享的资源。上面的a(%rip)表示全局变量a的值,%eax是一个寄存器。第一条指令表示把a的值保存在eax寄存器中,第二条指令对eax中的值加上1。
我们来考虑一个情况:现在有两个线程A,B,都要调用add()函数,那么a的预期值应该是2。如果线程A执行完第一条指令后,发生了上下文切换,此时eax寄存器的值是a的初始值0,CPU去执行线程B,线程B执行完毕后,把1返回给a(%rip),再恢复执行线程A,由于线程A不会再去执行第一条指令,因此eax寄存器的值不会被更新,依旧是0,线程A执行完毕后,把1返回给a(%rip)。最终,a(%rip)的值是1而不是2。
上面这种情况就是我们常说的数据不同步,或者线程不安全。对此,人们提出了很多方法,比如原子操作,互斥锁等等,出发点主要是以下两种:
- 保证操作不被线程调度机制打断,要么全部完成,要么全部不做;
- 即使操作被线程调度机制打断,别的线程也无法获得相关资源的使用权。
现在回到python这门语言上。荷兰人Guido van Rossum为了打发时间,于1989年发明了脚本语言python。和java类似,python源码首先会被编译成字节码(python项目中的pyc文件),然后由解释器进行解释。类似于在高级语言和汇编语言之间还多了一层中间语言。
上图中,一个python表达式可以由多个解释器指令构成,一个解释器指令又可以被分成多个汇编指令,这意味着一个解释器指令可能在执行过程中被打断,事实也确实是这样,python的解释器CPython并不是线程安全的。所以,为了保证线程安全,首先要做到让一个解释器指令能不受线程调度影响被执行完毕,对此python解释器的开发者们捣鼓出了python全局解释器锁,简称GIL。GIL在任一时刻只允许运行一个线程,当一个线程执行时间达到阈值时,释放GIL,这样连线程调度也变得简单了许多。
那么GIL是不是解决了线程安全的问题了呢?没有。这是python中的一个深坑。下一篇博客我会写一些自己在学习GIL时的心得。