python多进程_Python的GIL和多进程研究(1)

7d3c8e5a96d47c41a38216ffb22a2014.png

多线程多进程和协程,是Python中入门时候遇到的几个小坑,一般来说,我们想让代码同时执行多个任务,比如一边爬取某些网页的图片的URL插到队列内,一边让数据库从队列里读取URL入库,一边用下载器下载队列内的URL链接。这时候如果顺序执行就会因为下载图片和入库操作导致程序变的很慢,这时候我们会有三种方法操作:

1.多进程2.多线程3.多进程,每个进程下多个线程

但是啊,提到线程和进程,那就绕不开GIL,什么是GIL呢?GIL全称是 Global Interpreter Lock ,他是一把超级全局大锁,Python官方的解释链接在这:

https://wiki.python.org/moin/GlobalInterpreterLock
In CPython, the global interpreter lock, or GIL, is a mutex that protects access to Python objects, preventing multiple threads from executing Python bytecodes at once. This lock is necessary mainly because CPython's memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)

绝望不?兄弟萌,人家说这是防止多线程并发执行机器码的互斥锁。难顶啊,我们看看吧~

先说说什么是锁,锁这个词很形象了,一般来说,如果代码内多个线程公用某个变量,一会你改一会我改,没有互斥锁的话,大家一起改来改去变量就会改的稀烂,举个例子:

import threadingmoney = 0def money_activity(x):    global money    money = money + x    money = money - xdef test_thread(x):    for i in range(100000):        money_activity(x)if __name__ == '__main__':    t1 = threading.Thread(target=test_thread, args=(10,))    t2 = threading.Thread(target=test_thread, args=(15,))    t3 = threading.Thread(target=test_thread, args=(12,))    t1.start()    t2.start()    t3.start()    print(money)

上面这些代码,就是不加锁的后果演示,大家公用money这个全局变量,理论上一份代码运行出来的结果,应该是每次都一样,但是这份代码运行多次,每次结果都不一样,原因就是没有给money加锁,导致大家抢着操作money这个变量,导致变量被改乱了呗。

说完锁,再说Python的GIL机制,其实GIL也不完全是Python的问题,是因为CPython这个解释器的历史问题,就像C++有GCC啊VC的编译器一样,Python也有很多解释器,CPython这种解释器用的人太多了,几乎就是默认的解释器,所以GIL的锅也就被Python背上了。。。

GIL这把大锁实在是太大了,大到几乎让Python变成一个纯单线程的语言了,这是为什么呢?Python社区认为操作系统本身的线程调度已经非常成熟稳定了,没有必要自己搞一套,因此Python的线程实际上就是C语言的一个pthread,通过操作系统调度算法进行调度。为了让各个线程能够平均利用CPU时间,python会计算当前已执行的微码数量,达到一定某个阈值后就强制释放GIL。而这时也会触发一次操作系统的线程调度(当然是否真正切换还是由操作系统自主决定)。

上面的理论看着挺好的,但是呢,这对只是看起来对单核心的CPU比较美好,如果遇到多核心的CPU,那问题就大了,先看看Python线程的调度流程:

1.获取GIL2.执行定长时间片,我们假设是100个时间片3.释放GIL,立刻回到1

Python的线程调度操作的大概过程如上,我们发现了一个问题啊,如果核心0上的某线程此时正在执行,它到了第三步,再回到第一步,这两个步骤之间几乎没有任何时间间隙(或者说时间间隙超级短,短到可以忽略),这就会造成当在核心1上的线程被唤醒时,大部分情况下核心0上的线程已经又再一次获取到GIL了。这个时候被唤醒执行的线程只能白白的浪费CPU时间,被唤醒却没法干活,然后达到切换时间后进入待调度状态,再被唤醒,再等待,以此往复恶性循环。

但是以上仅针对CPU计算密集型的多线程有效,GIL的存在导致多线程无法很好的利用多核CPU的并发处理能力。

对于IO密集型的任务那就不一样了,一般情况下,IO有发送数据(output)和返回数据(input)两个过程。比如以浏览器为主体,浏览器发送请求给服务器(output),服务器再将请求结果返回给浏览器(input)。python在IO阻塞的情况下,会释放GIL,其他线程会在当前线程等待返回值(阻塞)的情况下继续执行发送请求(output),第三个线程又会在第二个线程等待返回值(阻塞)的情况下发送请求(output),即在同一时间片段,会有一个线程在等待数据,也会有一个线程在发数据。这就减少了IO传输的时间。

综上,得出结论,Python中的多线程适合IO密集型任务,而不适合计算密集型任务。那如果我偏要进行计算密集型的任务怎么办呢?答案:用多进程。

我认为multiprocessing库的出现很大程度上是为了弥补thread库因为GIL而低效的缺陷。它完整的复制了一套thread所提供的接口方便迁移。唯一的不同就是它使用了多进程而不是多线程。每个进程有自己的独立的GIL,因此也不会出现进程之间的GIL争抢,所以如果宁想做计算密集型的任务,那就多进程吧,IO密集型就用多线程。

a45db0e0c379d5928cd34fa97d178c99.png

接下来我们做个实验,证明一下在计算密集型的任务下,Python多线程属实拉胯(比二次元刀酱还拉跨):

# 循环做两次计数import timedef thread_test():    for i in range(1000000000):        i = i + 1    return iif __name__ == '__main__':    t_1 = time.time()    for i in range(2):        thread_test()    t_2 = time.time()    print(t_2-t_1)

a889ebc3973b303365acaa78aa38aabb.png

# 开两个线程import threadingimport timedef thread_test():    for i in range(1000000000):        i = i + 1    return iif __name__ == '__main__':    t_1 = time.time()    t1 = threading.Thread(target=thread_test)    t2 = threading.Thread(target=thread_test)    t1.start()    t2.start()    t1.join()    t2.join()    t_2 = time.time()    print(t_2-t_1)

a2545fb12801e9547eaef80f2c8f0eed.png

这特么也差太多了吧!接下来用多进程来做计算密集型任务试试咯:

from multiprocessing import Processimport timedef thread_test():    for i in range(1000000000):        i = i + 1    return iif __name__ == '__main__':    t_1 = time.time()    p1 = Process(target=thread_test)    p2 = Process(target=thread_test)    p1.start()    p2.start()    p1.join()    p2.join()    t_2 = time.time()    print(t_2-t_1)

cbd31401b838b5be866b2cdea4672322.png

看运行结果,嗯,爆炸式速度提升,这才是我想要的东西嘛~

下一篇好好说说协程这个玩意,协程属实难顶~

觉得好看的铁铁们可以关注一哈:

1f22827c7c0938dbf377c2185eda29f7.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值