GIL(全局解释锁)
GIL面试题入下:
描述Python GIL的概念, 以及它对python多线程的影响?编写一个多线程抓取网页的程序,并阐明多线程抓取程序是否可比单线程性能有提升,并解释原因。
参考答案:
- Python语言和GIL没有半毛钱关系。仅仅是由于历史原因在Cpython虚拟机(解释器),难以移除GIL。
- GIL:全局解释器锁。每个线程在执行的过程都需要先获取GIL,保证同一时刻只有一个线程可以执行代码。
- 线程释放GIL锁的情况: 在IO操作等可能会引起阻塞的system call之前,可以暂时释放GIL,但在执行完毕后,必须重新获取GIL Python 3.x使用计时器(执行时间达到阈值后,当前线程释放GIL)或Python 2.x,tickets计数达到100
- Python使用多进程是可以利用多核的CPU资源的。
- 多线程爬取比单线程性能有提升,因为遇到IO阻塞会自动释放GIL锁
示例:
如果Ubuntu设置的是四核,也就是说同时可以执行两个任务,那么下面可以进行试验:
试验1:运行如下代码
while True:
pass
这是一个死循环,在终端输入htop
发现原本是四核的Ubuntu,有一核被充满了,这就说明执行上述代码的时候单线程已经占用了一个核了,当终止运行的时候,会发现又回到了刚开始的状态。
试验2:运行下面代码
from threading import Thread
def test():
while True:
pass
def main():
th = Thread(target=test)
th.start()
while True:
pass
if __name__=='__main__':
main()
运行结果如下:
可以看到四个核的总占用大约就是100%,也就是说这个代码虽然是双线程的但是却不是真正的并发,就相当于有个互斥锁的存在,本质上来看还是单线程
试验3:运行下面代码
from multiprocessing import Process
def test():
while True:
pass
def main():
th = Process(target=test)
th.start()
while True:
pass
if __name__=='__main__':
main()
可以看到这个运行结果是一个核先占满,这就说明在进程中运行的时候才是真的并发运行,也就是是真的有多进程在运行,这是因为进程中没有GIL锁,而线程中有GIL锁
因为我们一般用的都是C写的Python解释器,这样的话就会有GIL锁的问题;之前所说的进程比线程占用更多的资源,但是进程可以实现真正的多任务,所以二者各有优缺点。如果是使用Java写的解释器那么就没有GIL锁的问题,当然选择线程会更好
线程虽然会有GIL锁的问题,但是一般来说多线程实现多任务还是比单线程更快的
那么什么时候用进程什么时候用线程?
- 计算密集型编程:代码中充斥着大量的计算,没有延时或者recv接收等待函数,这时候可以通过进程加快计算过程
- I/O密集型编程:即含有输入输出操作的编程,硬盘的读写速度相较于CPU的运算速度是非常慢的,也就是在运算过程中需要等待,这时候就可以用多线程来进行操作,某一线程运行过程中等待接收数据的同时其他线程可以进行输出或者计算,这样就可以节省时间
深拷贝和浅拷贝
浅拷贝是对于一个对象的顶层拷贝,通俗的理解是拷贝了引用但是没有拷贝内容
注意:浅拷贝和‘=’是有区别的---浅拷贝是对一个对象的顶层拷贝,[11, 22]进行浅拷贝之后只是把最顶层的也就是中括号那一部分进行拷贝,里边数据的id是一样的,但是‘=’是直接赋值一份对象的引用,也就是说没有进行拷贝(如果进行拷贝id值会发生改变)
深拷贝是对于一个对象所有层次的拷贝(递归)
拷贝的其他方式
分片表达式可以赋值一个序列
分片表达式和copy.copy()一样,都是属于浅拷贝
字典的copy方法可以拷贝一个字典,值共享,也就是不对值进行拷贝,都属于浅拷贝
注意点:
浅拷贝对不可变类型和可变类型的copy不同
- copy.copy对于可变类型,会进行浅拷贝
- copy.copy对于不可变类型,不会拷贝,仅仅是指向
In [88]: a = [11,22,33]
In [89]: b = copy.copy(a)
In [90]: id(a)
Out[90]: 59275144
In [91]: id(b)
Out[91]: 59525600
In [92]: a.append(44)
In [93]: a
Out[93]: [11, 22, 33, 44]
In [94]: b
Out[94]: [11, 22, 33]
In [95]: a = (11,22,33)
In [96]: b = copy.copy(a)
In [97]: id(a)
Out[97]: 58890680
In [98]: id(b)
Out[98]: 58890680
如果用copy.copy、copy.deepcopy对一个全部都是不可变类型的数据进行拷贝,那么结果相同,都是引用指向
如果拷贝的是一个拥有不可变类型的数据,即使元组在最顶层,那么deepcopy仍然是深拷贝,而copy.copy还是在指向引用
私有化
- xx: 公有变量
- _x: 单前置下划线,私有化属性或方法,f在本模块中可以访问,但是from somemodule import *时候不导入这些属性或者方法,类对象和子类可以访问
- __xx:双前置下划线,避免与子类中的属性命名冲突,只能在本类中进行访问,无法在外部直接访问(名字重整所以访问不到),子类和其他类无法进行访问。可以通过get函数访问或者直接通过‘_类名‘+‘私有属性名’进行访问
- __xx__:双前后下划线,用户名字空间的魔法对象或属性。例如:
__init__
__ 不要自己发明这样的名字,会被子类继承 - xx_:单后置下划线,用于避免与Python关键词的冲突
通过name mangling(名字重整(目的就是以防子类意外重写基类的方法或者属性)如:_Class__object)机制就可以访问private了。
#coding=utf-8
class Person(object):
def __init__(self, name, age, taste):
self.name = name
self._age = age
self.__taste = taste
def showperson(self):
print(self.name)
print(self._age)
print(self.__taste)
def dowork(self):
self._work()
self.__away()
def _work(self):
print('my _work')
def __away(self):
print('my __away')
class Student(Person):
def construction(self, name, age, taste):
self.name = name
self._age = age
self.__taste = taste
def showstudent(self):
print(self.name)
print(self._age)
print(self.__taste)
@staticmethod
def testbug():
_Bug.showbug()
# 模块内可以访问,当from cur_module import *时,不导入
class _Bug(object):
@staticmethod
def showbug():
print("showbug")
s1 = Student('jack', 25, 'football')
s1.showperson()
print('*'*20)
# 无法访问__taste,导致报错
# s1.showstudent()
s1.construction('rose', 30, 'basketball')
s1.showperson()
print('*'*20)
s1.showstudent()
print('*'*20)
Student.testbug()
总结:
父类中属性名为__名字的,子类不继承,子类不能访问,同一模块的其他类也不能访问
如果在子类中向__名字赋值,那么会在子类中定义一个与父类相同名字的属性
_名的变量、函数、类在使用from ## import * 时不会被导入,但是本模块中可以使用而且会被子类继承
import导入路径
import 搜索路径
- 从上面列出的目录里依次查找要查找的模块文件
- 第一行表示当前路径,也就是说默认是从当前路径进行优先查找
- 列表中的路径的先后顺序代表了Python解释器在搜索模块时的先后顺序
程序执行时添加新的模块路径
sys.path.append('/home/itcast/xxx')
sys.path.insert(0, '/home/itcast/xxx') # 可以确保先搜索这个路径
重新导入模块
模块被导入之后,如果被导入的模块发生了修改,如果说想要使用到被修改的内容,那么就需要重新导入模块。重复导入是不能将修改过后的模块重新导入的,需要reload