一. 为什么说在一个进程内的python多线程只能用单核(正常状况下 多线程是可以用多核cpu的)
进程是资源分配的最小单位,也是cpu调度的基本单位
线程是cpu调度的最小单位
正常的情况下:
注意多线程是可以用多核的(这时你可能会想,进程明明只有一个啊!)
这是我的理解
进程是调度的基本单位,但是线程是最小单位,也是可以调度的。现代操作系统以线程为调度的最小单位,所以 操作系统也会将其分配给空闲的cpu (这里也可以说是线程获得cpu的使用权)。cpu是操作系统中用来运算的一个部件,线程拿到cpu控制权就能进行运算,这里跟 多线程共用进程的资源空间没有关系。
python下的情况:
那么为什么python的多线程没法用到多核呢,因为有cpython解释器的GIL。它会使得在 同一个进程下 的线程们 同一时刻 只能有 一个线程在运行 (在执行字节码),就算是在多核cpu的系统下,因为线程们 共用的是一个进程的中的cpython解释器 ,那么就会被那个进程内运行的cpython解释器给限制(线程执行的是python代码,那么最终解释编译要经过解释器,就会被限制)。
总结
只要你用了一个进程内的cpython解释器,那么那个进程内的多线程就不能并行,只能并发,因为GIL(全局解释器锁)。不管是cpu密集型任务还是io密集型任务,io密集会变快只是因为io操作时,线程主动会释放锁给别的线程用而已。
在python中想要并行只能多进程,因为多进程下每个进程的资源空间都是独立的,cpython解释器也是多份的。
二. 新旧cpython版本的GIL释放规则
GIL基于 条件变量 和 互斥锁
简单的说这是两种线程间同步的机制
互斥锁 https://baike.baidu.com/item/%E4%BA%92%E6%96%A5%E9%94%81/841823?fr=aladdin
条件变量 https://baike.baidu.com/item/%E6%9D%A1%E4%BB%B6%E5%8F%98%E9%87%8F/8400584
旧GIL
解释器基于 ticker 来决定是否释放GIL(ticker数量默认为100),并且释放完后,释放的线程依旧会参与GIL争夺 (这里并没有把释放GIL的线程给排除在外) ,这就使得某线程一释放GIL就立刻去获得它,而其他CPU核下的线程相当于白白被唤醒,没有抢到GIL后,继续挂起等待,这就造成了资源的浪费
改进后的GIL
不再使用ticker,而改为使用时间,可以通过 sys.getswitchinterval()来查看GIL释放的时间,默认为 5 毫秒。 此外虽然说新GIL使用了时间,但决定线程是否释放GIL并不取决于时间,而是取决于 gildroprequest 这一全局条件变量,如果 gildroprequest=0,则线程会在解释器中一直运行,等 gildroprequest=1,此时线程才会释放GIL,然后发送信号(notify)给其它在睡眠挂起状态(wait)的线程,让它们去抢(acquire)。
python 多线程中 条件变量 的使用 https://www.cnblogs.com/yoyoketang/p/8337118.html
GIL分析以及源码解析参考(上) https://zhuanlan.zhihu.com/p/76343641
GIL分析以及源码解析参考(下) https://zhuanlan.zhihu.com/p/77674796
GIL源码解析 https://cyrusin.github.io/2016/04/27/python-gil-implementaion/
三. 为什么有了GIL还要关注线程安全
Cpython使用简单的锁机制避免多个线程 同时执行字节码 (注意!这里是字节码)
理解:cpython解释器限制了线程的并发,那么python代码经过解释器解释过后变成一堆字节码,此时批量字节码的执行并不是被限制的。可能你一个线程有 一个加的操作
加 这个操作比如:a += 1,这里面有 + 还有 = 两个指令,先要算出a+1的值,然后再把a+1的值覆盖a的值 (这里 “覆盖” 不太对,涉及到python中的变量是什么 这个事情,但是在这里姑且这么理解吧) ,这里面是包含多条字节码的。
但因为每时每刻只有一个线程在执行一条bytecode。一行python代码执行完了,但是你一行python代码解释出来的(可能是几行的字节码)还没执行完,此时另一个线程也开始执行相同的几行字节码了,那么如果涉及到修改共享的资源时,就会出错了,所以在python下要保证线程安全还是需要加锁之类的进行限制。
只是个人理解,有什么觉得不妥的地方,欢迎讨论😀