面试题
1 什么是gil锁
gil锁:全局解释器锁,他的本质是一个大的互斥锁,他是cpython的一种机制,gil只存在cpython解释器,他限制了一个线程只有获取到gil锁才能执行,如果没有拿到gil锁,线程是不能执行的
解释器有:cpython,pypython,jpython
gil锁的作用是什么?
限制线程只有获取到gil锁才能执行
为什么要有gil锁?保证数据安全?
保证数据安全用互斥锁就好了,gil锁不能保证数据安全
Python的垃圾回收机制,需要用逻辑回收线程来做,如果在同一时刻有多个线程在同时执行,垃圾回收机制会把正在其他线程使用的变量给回收掉,因为当时只有单核电脑,本来同一时刻就不能有多个线程同时执行,于是作者就干脆做了一个gil锁,让线程必须获取到gil锁,才能执行,但后来随着多核的出现,导致python的多线程并不能利用多核
2 为什么有了gil锁还要互斥锁
gil锁:全局解释器锁,线程要想执行,必须先获得到gil锁才能执行
互斥锁:为了保证多线程并发操作数据(变量),而设置的锁,保证在加锁和释放锁之间,其他线程不能操作
# gil锁也是大的互斥锁
# 过程:
# 出现了数据错乱,出现了多条线程操作变量,出现的并发安全问题
a=0
线程1要计算: a+=1
1 线程1 拿到gil
2 读取a=0
3 假设时间片到了,释放gil,释放cpu
4 等待下次被调度执行
10 轮到它了,获取gil锁
11 继续往下执行:计算a+1
12 把结果赋值给a ,a=1
13 释放gil锁
线程2要计算: a+=1
5 线程2获得了gil锁
6 读取a=0
7 计算a+1
8 把结果赋值给a ,a=1
9 释放gil锁
# 什么临界区?处出现并发安全问题的这段代码称之为临界区,临界区会出现并发安全问题,所以要在临界区加锁
# 加锁
6 读取a=0
7 计算a+1
8 把结果赋值给a ,a=1
# 释放锁
# 互斥锁保证数据安全
a=0
线程1要计算: a+=1
1 线程1 拿到gil
# 加锁
2 读取a=0
3 假设时间片到了,释放gil,释放cpu
4 等待下次被调度执行
7 轮到它了,获取gil锁
8 继续往下执行:计算a+1
9 把结果赋值给a ,a=1
10 释放gil锁
线程2要计算: a+=1
5 线程2获得了gil锁
#获取锁,获取不到
6 释放gil锁
11 获得gil锁
#加锁
12 读取a=0
13 计算a+1
14 把结果赋值给a ,a=1
15 释放锁
16 释放gil锁
# gil锁并不锁住临界区,临界区需要我们自己用互斥锁加锁
3 python的垃圾回收机制是什么样的?
高级一点的语言,为了保证内存的使用效率,都会有垃圾回收机制,而咱们python使用以下三种方式来做垃圾回收
1.引用计数
有多少变量指向他,他的引用计数就为几,当引用计数为0 的时候就说明没有变量指向它了,这块内存空间就会被回收掉
引用计数操作的问题:循环引用问题
2.标记清除
为了解决引用计数存在的循环引用的问题
第一阶段就标记阶段,它会把所有的“活动对象”打上标记,第二阶段把那些没有标记的“非活动对象”进行回收
# 简而言之,它会把循环引用的内存空间,打上标记,然后回收掉
3.分代回收
把对象分为三代,一开始,对象在创建的时候,放在一代中,如果在一次一代的垃圾回收检查中,该对象存活下来,就会被放到二代中,同理在一次二代的垃圾检查中,该对象存活下来,就会被放到三代中,后面优先检查第一代中的对象,优先回收,其次依次往上检查做回收
4 解释为什么计算密集型用多进程,io密集型用多线程
由于GIL锁的存在,即便是多核机器,同一时刻,也只能有一个线程在执行
-线程需要cpu去调度执行
-如果开了多线程,是计算密集型,计算是消耗cpu,假设是四核电脑,不能充分利用这四个核,只能有一个核在执行,开多线程没有用
-而如果计算密集型,开了多进程,gil是在cpython解释器进程中的,再进程中开启线程执行计算,可以充分利用多核优势
-开了一个进程,就开了一个cpython解释器的进程,就会有一个gil锁
-由于GIL锁的存在,如果是计算密集型,开启多线程,不能利用多核优势,
-开启多进程,可以利用多核优势
-io不耗费cpu,开启多线程,在一个时间段内是有并发效果的
-即便是io密集型,用多进程是不是会显著提高效率?
本身开启进程是非常消耗资源的,如果是io密集型,没有必要开多进程,并不会有显著的效率提升
5 进程,线程和协程
概念
进程:是资源分配的最小单位,一个应用程序应用起来,至少需要一个进程,在进程管理器(资源管理器)中可以看到一个个进程
线程:是cpu调度,执行的最小单位,一个进程下至少有一个线程
协程:单线程下的并发,程序层面控制的任务切换
代码如何实现
# 开启进程的两种方法:
1.写一个类,继承Process,重写类的run方法,实例化得到对象,对象.start开启进程
2.通过Process类实例化一个对象,传入一个任务,调用对象.start开启进程
# 开启线程的两种方法:
1.写一个类,继承Thread,重写类的run方法,实例化得到对象,对象.start开启线程
2.通过Thread类实例化一个对象,传入一个任务,调用对象.start开启进程
# 开启协程
1.早期的写法:借助于第三方gevent,基于greelet写的
2.现在写法:
有async和await关键字,不借助于第三方,开启协程asyacio包
写在一个函数前,async def task()--->这个函数执行的结果就是协程函数
await只要是io操作的代码,前面必须加await
三者我们在哪里用过
1.一般遇到计算密集型的操作,我会开多进程,遇到io密集型的操作,我会开多线程
2.闲着没事干的时候,爬取别的数据,喜欢开多线程,爬虫io居多
3.程序中,异步做一件事情,也可以开多线程
eg:
一个视图函数,异步的把数据写到文件中
异步的发送钉钉通知
异步的发送邮件
实际生活中,在项目中,不需要我们来开启进程线程的,可以借助于第三方的框架比如celery就可以进行异步操作
celery的worker,就是进程线程架构
4.django框架,是支持并发的,我们没有开启多进程、多线程,但是符合uwsgi的web服务器在进入Django框架之前,就开启了线程和进程来执行视图函数
6 什么是鸭子类型
走路像鸭子,说话像鸭子,我们就可以叫它鸭子
解释:鸭子类型是Python面向对象中描述接口的一个概念,区分与其他编程语言
eg:
java中:实现接口,必须显示继承一个接口
Python中:实现接口,遵循鸭子类型,不需要显示的继承一个接口(类),只要类中有对应的属性跟方法,我们就称这几个类的对象为同一种类型