随着多核时代的到来,怎样充分利用好你的多个CPU的优势成了技术的关注点,那就是多线程多进程编程,二者的区别也很明显,进程是操作系统中拥有资源的最小单位,但是是重量级的。线程是系统调度的最小单位,是轻量级的,一个进程可以拥有很多个线程,但是线程是不拥有资源的,同一个进程中的线程共享这个进程中拥有的资源。以前学习java,一个灰常重要的并发方式就是多线程,因为线程的开销要比进程的少很多,而通过加锁来保证线程安全,进而有线程池来做进一步的优化。
所以前面也花了些时间对python的多线程编程进行了了解,然后又了解了一下python的第三方库线程池threadpool。但是了解后面发现这些都白费了,虽然python提供了thread和threading库来支持多线程的实现,但是python最终还是没办法达到多线程并发的,原因就是GIL。
网上关于GIL的讨论也有很多,我也做了进一步的了解,这里有个例子可以看一下,就是python一个线程的死循环和开启多个线程来执行死循环对于CPU的消耗是差不多的,我的电脑是双核的,来测试一把:
程序运行前cpd的使用率:
开一个线程来执行死循环,主线程也执行死循环cpu的使用率:
一个线程运行死循环程序也是用了50%到60%的cpu,增多一个线程来跑死循环还是用了50%到60%的cpu,都没什么变化。这是为什么呢?原因就是GIL。
1.何方神圣
GIL全称是Global Interpreter Lock,即全局解释锁,每个python解释器都有一个这样的全局锁,在解释器解释执行任何的python代码都要先获得这个锁。所以同一时间只有一个线程持有这个锁在运行,其他的线程都在等待这个锁。所以虽然有两个线程在执行死循环,但实际上每次只有一个线程在运行。
2.要它何用
既然GIL如此的让人蛋疼,以至于多线程都不能发挥多核的优势,那么还要GIL有何用呢。我们只需要像java一样让程序猿自己在访问共享变量处加锁不就可以了吗,为毛一下把整个解释器给锁上了,这不是自找麻烦么。python是解释性语言,是在python解释器上执行的,但是python解释器本身却不是线程安全的,如果没有GIL,那么在多线程中一些简单的操作都会出现问题,在可见变量我们可以自行加锁,而还有很多后台不可见的变量就需要GIL来控制了,比如为了进行垃圾回收而维护的引用计数,如果没有GIL,有可能出现由于线程切换导致的对同一个对象释放两次的情况。所以有了Lock还是需要GIL的。
如果将GIL抛弃,所有的地方都用Lock也可以啊,这样就可以支持多线程并发了,曾有过官方实验发现这样的做法是得不偿失的,单线程的执行效率减慢了将近两倍,而且这样的做法却复杂了N倍,本来一个全局锁就可以解决的问题却花了N多个细微的锁来解决。所以这样是不值得的,何况python还有别的方式来实现并发。
3.有了GIL要Lock何用
既然有了这样一个全局锁来保护了python解释器,让每次只能有一个线程能够得以执行,那么对于共享资源来说,每次也只能有一个线程来访问它,那么python提供的Lock还有用么?就比如这篇博客中第一个例子,不加锁的话结果是不可知的,但是是不会达到一百万的。这又是为什么呢,不是已经有了一个全局锁来保护共享资源吗?
在网上搜到别人的说法是python中线程是对外抽象出来的,而python内部还会想操作系统一样调度这些线程,所以还是需要Lock来保证这些线程间的安全。如果是这样的话倒也说的通,但是python的线程明明是操作系统的原生线程啊。这是为什么呢,这个问题一直困扰着我啊。
4.并发其他途径
多线程是python并发的痛,看来不可能实现的了,但是可以通过其他的途径来实现并发编程,虽然进程较线程是重量级的,但是进程之间是相互独立的,不存在资源共享,所以就没有了互斥的概念,也就不需要锁了,所以还可以通过多进程+协程来实现并发的。
进程并行或者线程并行
各有优缺点,要看情况,不是绝对的,在此不讨论这个,这引出下面两种Python并行处理方法(注释感觉很清晰详细了,不再多说)
0x02 进程处理方法