黑马程序员学Python——GIL锁与深浅拷贝

本文主讲:Python中的GIL锁以及怎么去解决这个问题,深浅拷贝等….

GIL锁对多任务的影响【重点】

unubtu 系统的 htop 指令查看CUP的使用情况(百分比)

指令:终端输入 htop 即可

图示:

image-20211012155420672

单进程单线程死循环、多进程单线程死循环、多线程单进程死循环对CPU核的调用情况

1.单进程单线程死循环使用情况:

结论:单进程可完美利用cpu的核数

2.多进程单线程

结论:两个多进程死循环运行,两个cpu核利用率为百分之百,因此多进程可完美利用CPU的核数,不存在 cpu 利用不充分的问题哦!

image-20211012155447877

3.多线程单进程

结论:多个线程同时运行死循环函数,但是CPU的核却没有一个占满百分之百,可以说明多线程是伪多线程,并不能同时运行多个任务,这是因为GIL全局解释器锁的原因

图示:

image-20211012155458246

GIL锁的概念及影响【重点、面试点】

什么是GIL

答:GIL称为全局解释器锁,python执行多线程任务之前,某线程会获得一把GIL,而且GIL又是一把互斥锁,某个线程执行任务调用cpu核资源时,其他线程就不能执行其他任务,只能等待GIL释放完毕才会执行任务,保证同一时间只有一个线程在使用GIL,简而言之,每个执行任务的线程都会获得一把GIL,有且整个程序只有一把GIL

注意:GIL与python语言无关,而是与cpython解释器有关,比如jpython解释器就没有这个GIL

GIL存在的原因

1:编写PYTHON的时候计算机普遍是单核cpu
2:不加GIL执行多线程任务容易引起资源竞争

GIL对多任务的影响

多线程:造成伪多线程,实际同一时间只有一个cpu内核在轮询执行任务,保证同一时间只有一个线程可以执行任务

多进程:不会造成影响,两个进程,那就调度两个cpu内核执行这两个进程任务

在什么时候可以释放GIL

1:获得全局解释器锁的线程任务执行完毕

2:获得全局解释器锁的线程超过时间片轮询时间,自动释放GIL,执行其他任务

3:I/O操作阻塞时自动释放GIL

4:获得全局解释器锁的线程执行阻塞时自动释放GIL

你问我答:因为GIL也是一把互斥锁,那么是否意味着我们在操作多线程全局变量时不用添加互斥锁了呢?

答:错误,因为无法控制GIL锁释放的时间,不能确保使用完全局变量的操作是否已经完成!

课后习题:因为GIL存在的原因,多线程相当与伪多线程,那么为什么多线程的执行效率比多线程高呢?

答:因为多线程执行任务时遇到I/O操作阻塞等原因会自动释放GIL

GIL解决方案【重点、面试点】

python多线程解决GIL问题的几种方案

方案一:因为GIL是 python 底层解释器 Cpython 产生的,因此可以更换其他底层解释器,例如 jpython 【不推荐】
不推荐原因:python默认解释器就是CPython,而不是Jpython,这肯定是有原因的

方案二:将多线程改成多进程【推荐】

优点:每一个进程都拥有一把GIL,不会因为互斥锁特性造成同一时间只有一个线程在执行任务,可以解决多线程利用 cpu 核不充分的问题

缺点:进程是资源分配的基本单元,改成多进程会消耗更多的内存资源,当计算机内存紧张是不推荐使用!

方案三:将多线程执行的任务(函数)改用 c语言代码编写,可避免GIL!

详解:python是胶水语言,将 c文件编译为 so 文件,并在python内导入 ctypes 模块即可在python内执行 c文件代码!

实现步骤:

第一步:在pycharm内创建后缀为 c 的C语言文件 new → file →文件名.c 编写c语言任务代码(多线程执行任务的代码)

image-20211012155549163

第二步:在 unubtu 终端内将后缀为 c 的文件编译为后缀为 so 的文件

指令关键字:gcc

语法:gcc 后缀为c的文件名 -shared -o 待生成的后缀为so的文件名

注意事项:

1:待生成的后缀为so的文件名一般都是以lib三个字母开头

2:需要进入c文件所在的文件夹内才能执行此命令

3:-shares选项代表将目标文件编译为so文件

4:-o选项是用来输入的文件名选项

截图:

image-20211012155558587

第三步:在python程序内导入 ctyes 库并导入 so 文件从而执行so文件内的多线程任务

关键语法:

1
2
import ctypes
lib = ctypes.cdll.LoadLibrary(“./so文件名”)

注意事项:
1:导入so文件时,一定要在相对位置目录导入,并且要加上 ./

代码演示:

image-20211012155611363

第四步:执行代码查看cpu核的利用率判断是否解决了GIL

image-20211012155618933

多线程的适用场景

适用场景:I/O密集型可使用多线程(GIL锁会释放)
不适用场景:CPU密集型不建议使用多线程(CPU利用率不高)

python可变与不可变【重点】

python中可变与不可变数据类型的分类以及区别

可变数据类型:列表、字典

不可变数据类型:数字、字符串、元组

可变与不可变数据类型的区别:

答:

1.可变数据类型在内存创建变量后,如果这个变量发生改变(增加数据等),内存不会开辟新的空间来存放变化后的数据,而是在原位置增加内存空间用来存放变化后的数据

2.不可变数据类型在内存创建变量后,如果这个变量发生变化,内存会开辟一块新的空间用来存放变化后的数据,而这个变量也会执行这个新的内存地址

简而言之:可变数据类型在发生变化后,不会在内存中开辟新地址存放数据,不可变数据类型在数据发生变化后,会开辟新的内存地址存放数据,并且原地址数据会被内存释放掉

注意事项:

1:变量名指向的是变量的内存地址

2:可变与不可变是数据发生改变后内存地址是否发生变化

图示:

image-20211012155709565

怎么用python代码验证可变与不可变数据类型

关键字:id(变量名)

方法:通过对比数据发生变化前后的内存地址变化从而确定是否为可变数据类型

代码演示:

不可变数据类型

image-20211012155721184

可变数据类型

image-20211012155728120

数据的浅拷贝与深拷贝区别【重中之重】

数据的深浅拷贝区别

浅拷贝:只拷贝数据的内存地址(引用),不会开辟新的内存空间,不能保证数据的独立性,如果拷贝的是对象【列表的嵌套】,原对象和copy对象都指向于同一个内存空间,不会拷贝对象内部的子对象

深拷贝:拷贝后会开辟新的内存空间存放拷贝后的数据,如果拷贝的是对象【列表的嵌套】,原对象和copy对象指向不同的内存空间,会拷贝对象及其子对象

注意事项:上述的深浅拷贝只是相对的,有些数据类型不能保证其正确性,比如简单可变数据类型(列表)的浅拷贝就会产生新的内存空间!

python怎么实现深浅拷贝

关键字:

浅拷贝:copy(变量名)

深拷贝:deepcopy(变量名)

语法:

1
2
3
4
import copy
list1 = [1,2,3,4]
list2 = copy.copy(list1) 浅拷贝
list3 = copy.deepcopy(list1) 深拷贝

图示:

image-20211012155801753

简单可变类型深浅拷贝【重点】

简单可变类型(列表)的深浅拷贝特点

列表浅拷贝特点:产生新的内存空间,原列表与拷贝后的列表数据是相互独立的(互不影响),这与浅拷贝定义是不一样的一定要注意

列表深拷贝特点:产生新的内存空间,原列表与拷贝后的列表数据是相互独立的(互不影响)

注意事项:

1:浅拷贝的定义是 对原数据进行拷贝不会产生新的内存空间,只是对原数据地址的拷贝,但是列表的浅拷贝却产生了新的内存空间

2:拷贝后产生新的内存空间就是两个数据的内存地址不一样

图示:

浅拷贝

image-20211012155819140

深拷贝

image-20211012155825294

复杂可变类型深浅拷贝【重点】

复杂可变类型【列表的嵌套】的深浅拷贝的区别

注意:列表的嵌套可看做是一个对象,这样就更好的理解深浅拷贝定义的后两句话了!

列表的嵌套浅拷贝:A是一个列表的嵌套对象,其中的子列表地址指向的是原列表的地址,相当于地址的引用,对A列表对象进行浅复制会产生新的内存空间,但是A列表内的子列表经过浅复制后不会产生新的空间,依旧是原列表地址的引用,因此对原列表数据进行更改,浅复制后的数据将发生变化!!

简而言之:浅拷贝不会拷贝嵌套列表的子列表,只拷贝了地址(引用子列表)

图示:

image-20211012155839315

代码演示:

注意:下面代码没有演示对A列表数据改变,观察C[0]与D[0]的数据变化 结果是也变化

image-20211012155847748

列表的嵌套深拷贝:对嵌套的列表进行深复制会产生新的内存空间,并且嵌套的字列表也会产生新的内存空间,不再是地址的引用复制,这样对原数据进行更改,深复制后的数据将不再发生变化

图示:

image-20211012155856726

代码演示:

image-20211012155903218

简单不可变类型深浅拷贝【重点】

简单不可变数据类型(元组)的深浅拷贝

答:简单可变数据类型(元组)不管是深拷贝还是浅拷贝都不会产生新的内存空间,只是单纯的拷贝地址(引用源对象),拷贝前后的数据也不是相互独立的

代码:

image-20211012155911751

复杂不不可变类型深浅拷贝【重点】

目标:掌握复杂不可变类型(元组内嵌套列表)的深浅拷贝对比

复杂不可变类型(元组内嵌套列表)的深浅拷贝对比

元组内嵌套列表的浅拷贝:浅拷贝后元组不会产生新的内存空间,并且元组内嵌套的列表也不会产生新的内存空间,只是单纯的拷贝地址,原对象的引用!浅拷贝前后的数据也不是相互独立的!

代码:

image-20211012155924656

元组内嵌套列表的深拷贝:元组深拷贝之后会产生新的内存空间存放复制后的元组,并且元组内的列表也会开辟新的内存空间,不再是单纯的地址复制,这点从复制前后列表的地址不同可看出,而且复制后两个元组的数据是相互独立的(更改原元组内列表的数据,复制的元组列表内数据不会发生变化)

代码:

image-20211012155937171

重点:

1:复杂不可变类型数据(元组内嵌套数据)的深拷贝,如果元组内嵌套的是可变数据类型(列表)就会开辟新的内存空间保存可变的嵌套数据,如果嵌套的是不可变数据类型(元组),则不会开辟新的内存空间存放嵌套的数据!

2:复杂不可变类型数据(元组内嵌套数据)的浅拷贝,不管元组内嵌套的是可变还是不可变数据类型,都不会开辟新的内存空间,直接内存地址的引用,类似于超链接

切片拷贝、字典拷贝【重点】

简单可变数据类型(列表)的切片拷贝是深拷贝还是浅拷贝

切片拷贝示例代码:

list1 = [1,2,3]

list2 = list1[ : ]

list2就是list2的切片拷贝

实验结论:简单可变数据类型(列表)的切片拷贝是深拷贝

代码体验:

image-20211012155954539

复杂不可变数据类型(元组内嵌套列表)的切片拷贝是深拷贝还是浅拷贝

结论:是浅拷贝

代码演示:

image-20211012160004682

简单可变数据类型(字典)的字典拷贝是深拷贝还是浅拷贝

关键字:copy

语法:dict2 = dict1.copy()

注意事项:字典拷贝不需要导入 copy 模块即可使用 copy 方法

结论:简单可变数据类型(字典)的字典拷贝是深拷贝

代码演示:

image-20211012160018221

复杂可变数据类型(字典内嵌套列表)的字典拷贝是深拷贝还是浅拷贝

结论:复杂可变数据类型(字典内嵌套列表)的字典拷贝是浅拷贝

代码图示

image-20211012160030517

未完待续…….