从GIL开始重新认识Python多线程编程

时间:2022-12-22 01:00:47


我们想要了解Python得多线程,就必须要了解GIL,GIL即全局解释锁

举个栗子

计算机执行程序时,是需要把代码编译成机器指令再去执行的,我们现在用的编辑器,其实就是一种解释器,在我们右键运行程序时,它能够将整个文件编译成字节码,再由Python虚拟机来执行字节码,最后得到输出:

从GIL开始重新认识Python多线程编程


来看一下这个函数的字节码:

从GIL开始重新认识Python多线程编程


Python中有多个线程在同一时间运行同一段代码的时候呢,其实是很容易出错的,所以Python语言在早期的时候为了解决这一问题,便在解释器里加了一个锁,这个锁能够使得在同一时刻只有一个线程在CPU上面去执行这个字节码。也就是说,同一时刻只能有一个线程在一个cpu上面执行字节码。也正因如此,Python在执行多线程任务时,有人会觉得它慢。这样一来,无法显示出多核cpu的优势:

从GIL开始重新认识Python多线程编程


接下来我们看看有没有什么办法能解决这个问题,Python有一个内置的模块threading,它是专门用来解决多线程问题的:

import threading

a=0
def time():
global a #声明全局变量
for item in range(1000000):
a+=1

def test():
global a
for item in range(1000000):
a -= 1

thread_1=threading.Thread(target=time)
thread_2=threading.Thread(target=test)

thread_1.start()
thread_2.start()

thread_1.join()
thread_2.join()
print(a)

按理来说,这个程序的输出结果应该为0,可是:

从GIL开始重新认识Python多线程编程


而且,每次运行时,结果都不一样:

从GIL开始重新认识Python多线程编程


从GIL开始重新认识Python多线程编程


通过上面的运行结果,我们可以看出,整个线程并不是一旦占有就一直占有的!,就好像谈恋爱,有可能会分手一样,你以为能走到最后,可结果…

因此,GIL在某些情况下是可以被释放掉的:

  1. GIL会根据执行的字节码行数以及时间片进行释放
  2. 程序会在遇到IO操作的时候 ,会主动释放 GIL
什么是时间片?

比如说time这个线程,会被分配一个时间段来执行,这个时间段即时间片,也就是说,时间结束后,会进入到下一个线程,保证CPU资源不浪费

那么我们怎么进行多线程呢?

先来看看错误的写法:

import time
import threading

def get_data_html():
print('开始获取html数据的时间')
time.sleep(2)
print('获取html数据结束的时间')

def get_data_url():
print('开始获取url数据的时间')
time.sleep(2)
print('获取url数据结束的时间')

if __name__ == '__main__':
thread_1=threading.Thread(target=get_data_html)
thread_2=threading.Thread(target=get_data_html)
start_time = time.time()
thread_1.start()
thread_2.start()
print("中间运行的时间:{}".format(time.time() - start_time))

从GIL开始重新认识Python多线程编程


按理说,程序应该执行2秒,可是并没有,我们来debug一下:

从GIL开始重新认识Python多线程编程


其实这里应该有三个线程,最后一个输出语句是主线程,当主线程退出的时候,子线程会被kill掉了,因此线程没有执行完毕,那么我们可以模块内置的功能去守护线程,让线程继续运行:

if __name__ == '__main__':
thread_1=threading.Thread(target=get_data_html)
thread_2=threading.Thread(target=get_data_url)
thread_1.setDaemon(True) #守护线程
thread_2.setDaemon(True)
start_time = time.time()
thread_1.start()
thread_2.start()
print("中间运行的时间:{}".format(time.time() - start_time))

可是问题又来了:

从GIL开始重新认识Python多线程编程


没有完整地得到结果,我们试着关掉一个线程保护:

if __name__ == '__main__':
thread_1=threading.Thread(target=get_data_html)
thread_2=threading.Thread(target=get_data_url)
# thread_1.setDaemon(True)
thread_2.setDaemon(True)
start_time = time.time()
thread_1.start()
thread_2.start()
print("中间运行的时间:{}".format(time.time() - start_time))

从GIL开始重新认识Python多线程编程

为什么会出现这样的输出?

原因很简单,守护了thread_2,那么thread_2便不会自动结束,它将一直占用CPU,导致thread_1结束时,thread_2还没有结束

下面我们守护thread_1:

从GIL开始重新认识Python多线程编程


发现两个线程都能输出?这里我们改一下time.sleep()的时间,我们让thread_2的时间减少后再试一次:

从GIL开始重新认识Python多线程编程


总的来说,守护线程能避免当主线程退出的时候,子线程会被kill掉的情况

不过,即便如此,这样的方式仍然不是我们想要的,我们希望在两个线程都执行完以后,再来执行主线程

How to do it ?

线程阻塞能解决这个问题:

if __name__ == '__main__':
thread_1=threading.Thread(target=get_data_html)
thread_2=threading.Thread(target=get_data_url)
start_time = time.time()
thread_1.start()
thread_2.start()
thread_1.join()
thread_2.join()
print("中间运行的时间:{}".format(time.time() - start_time))

从GIL开始重新认识Python多线程编程


这里也可以看出,运行的时间并不是两个线程的耗时相加

今天的内容就到这里,下一篇文章将具体介绍多线程编程的应用案例。