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

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

举个栗子

计算机执行程序时,是需要把代码编译成机器指令再去执行的,我们现在用的编辑器,其实就是一种解释器,在我们右键运行程序时,它能够将整个文件编译成字节码,再由Python虚拟机来执行字节码,最后得到输出:
在这里插入图片描述
来看一下这个函数的字节码:
在这里插入图片描述
Python中有多个线程在同一时间运行同一段代码的时候呢,其实是很容易出错的,所以Python语言在早期的时候为了解决这一问题,便在解释器里加了一个锁,这个锁能够使得在同一时刻只有一个线程在CPU上面去执行这个字节码。也就是说,同一时刻只能有一个线程在一个cpu上面执行字节码。也正因如此,Python在执行多线程任务时,有人会觉得它慢。

这样一来,无法显示出多核cpu的优势:
在这里插入图片描述
接下来我们看看有没有什么办法能解决这个问题,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在某些情况下是可以被释放掉的:

  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))

在这里插入图片描述
按理说,程序应该执行2秒,可是并没有,我们来debug一下:
在这里插入图片描述
其实这里应该有三个线程,最后一个输出语句是主线程,当主线程退出的时候,子线程会被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))

可是问题又来了:
在这里插入图片描述
没有完整地得到结果,我们试着关掉一个线程保护:

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))

在这里插入图片描述

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

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

下面我们守护thread_1:
在这里插入图片描述
发现两个线程都能输出?这里我们改一下time.sleep()的时间,我们让thread_2的时间减少后再试一次:
在这里插入图片描述
总的来说,守护线程能避免当主线程退出的时候,子线程会被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))

在这里插入图片描述
这里也可以看出,运行的时间并不是两个线程的耗时相加

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

发布了60 篇原创文章 · 获赞 123 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/zbp_12138/article/details/104757800