1. GIL(全局解释器锁)
1.1 什么是GIL
在CPython中,这个全局解释器锁,也称为GIL,是一个互斥锁,防止多个线程在同一时间执行Python字节码。换句话说,这个锁就是用来锁住python解释器的。
1.2 GIL带来的问题
GIL加到了解释器上,并且是一把互斥锁,那么这把锁对应用程序就会产生一定的影响
py文件中的内容本质都是字符串,只有在被解释器解释时,才具备语法意义,解释器会将py代码翻译为当前系统支持的指令交给系统执行。
当进程中仅存在一条线程时,GIL锁的存在没有不会有任何影响,但是如果进程中有多个线程时,GIL锁就开始发挥作用了
由于互斥锁的特性,程序串行,保证数据安全,降低执行效率,GIL将使得程序整体效率降低!
1.4 GIL与GC
GC:python自带的内存管理机制。Python中内存管理使用的是引用计数,每个数会被加上一个整型的计数器,表示这个数据被引用的次数,当这个整数变为0时则表示该数据已经没有人使用,成了垃圾数据。当内存占用达到某个阈值时,GC会将其他线程挂起,然后执行垃圾清理操作,垃圾清理也是一串代码,也就需要一条线程来执行。
GC与其他线程都在竞争解释器的执行权,而CPU何时切换,以及切换到哪个线程都是无法预支的,这样一来就造成了竞争问题。
当然其他一些涉及到内存的操作同样可能产生问题问题,为了避免GC与其他线程竞争解释器带来的问题,CPython简单粗暴的给解释器加了互斥锁。有了GIL后,多个线程将不可能在同一时间使用解释器,从而保证了解释器的数据安全。
1.5 为什么Cpython要这么设计???
Cpython诞生于1991年 而多核处理器诞生2004年
当时不需要考虑多核效率问题
现在为什么不拿掉这个锁,因为这期间,很多已经完成的代码都依赖于这个锁,如果直接拿到,这些代码全得改,成本太大了
1.6 GIL的加锁与解锁时机
加锁的时机:在调用解释器时立即加锁
解锁时机:
该线程任务结束
当前线程遇到了IO时释放
当前线程执行时间超过设定值时释放 默认100纳秒
GIL的优点:
-
保证了CPython中的内存管理是线程安全的
GIL的缺点:
-
互斥锁的特性使得多线程无法并行
1.7 解决方案:
区分任务类型
如果是IO密集使用多线程
有一个下载任务 要从网络中下载一个文件 大小1G和转换 任务 使用input 转为大写输出
上述任务并行执行,耗时也不会有太大的提升,反而开启多进程会浪费更多资源这种任务称之为
IO密集型,大量的时间都花在IO等待
这类问题使用多线程,
如果是计算密集使用多进程
图像处理,语音处理,大数据分析,
1.8 GIL锁与自定义锁的关系
都是互斥锁
为什么有了GIL还需要自己加锁
GIL是加在解释器上的,只能锁住,解释器内部的资源,但是无法锁住我们自己开启资源
自定义锁:自己开启共享资源还得自己所锁
2. 进程池与线程池
池表示一个容器,本质上就是一个存储进程或线程的列表
2.1 为什么需要进程/线程池?
线程/进程池不仅帮我们控制线程/进程的数量,还帮我们完成了线程/进程的创建,销毁,以及任务的分配
mport time from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor from threading import enumerate,current_thread # 创建池子,可以指定池子里面有多少线程,如果不指定默认为CPU的个数 * 5 # 不会立即开启线程,会等到有任务提交后再开启线程 pool = ThreadPoolExecutor(2) # 线程池最大值,应该机器所能承受的最大值 # print(enumerate()) # 检测当前存活的线程 [<_MainThread(MainThread, started 11148)>] def task(i): print("%s is running" % i) time.sleep(3) for i in range(5): pool.submit(task,i) def task1(name,age): print(name) print(current_thread().name, "run") # ThreadPoolExecutor-0_0 run print(age) pool.submit(task1,"jerry",30) time.sleep(5) print(enumerate()) """ 线程池,不仅帮我们管理了线程的开启和销毁,还帮我们管理任务的分配 特点: 线程池中的线程只要开启之后 即使任务结束也不会立即结束 因为后续可能会有新任务 避免了频繁开启和销毁线程造成的资源浪费 1.创建一个线程池 2.使用submit提交任务到池子中 ,线程池会自己为任务分配线程 """ # 进程的使用 同样可以设置最大进程数,默认为CPU数 pool = ProcessPoolExecutor() def task2(name): print(os.getpid()) print(name) if __name__ == '__main__': pool.submit(task2,"wangyong") pool.shutdown() # 等待所有任务全部完毕 销毁所有线程 后关闭线程池 # pool.submit(task2,"jerry") # 线程已经被销毁,所以会报错
3. 同步异步----阻塞非阻塞
3.1 阻塞非阻塞指的是程序的运行状态
阻塞:当程序执行过程中遇到了IO操作,在执行IO操作时,程序无法继续执行其他代码,称为阻塞!
3.2 同步-异步 指的是提交任务的方式
异步指调用:发起任务后必须不用等待任务执行,可以立即开启执行其他操作
同步会有等待的效果但是这和阻塞是完全不同的,阻塞时程序会被剥夺CPU执行权,
而同步调用则不会!
4. 异步回调
4.1 什么是异步回调
异步回调指的是:在发起一个异步任务的同时指定一个函数,在异步任务完成时会自动的调用这个函数
4.2 为什么需要异步回调
4.3 异步回调的案例分析
线程池异步回调
""" 爬虫是干啥的 从网络中下载某个页面数据 在从页面中提取有价值的数据 """ """ 为嘛使用异步回调 1.生产者和消费者解开了耦合 2.消费者可以及时处理生产完成的数据 """ import requests from concurrent.futures import ThreadPoolExecutor from threading import current_thread pool = ThreadPoolExecutor(2) # 获取数据 def get_data(url): response = requests.get(url) print("%s 下载完成" % current_thread().name) return url,response.text # 解析数据 def parser(obj): url,data = obj.result() print("%s 长度%s" % (url,len(data))) print("%s解析完成!" % current_thread().name) urls = [ "http://www.baidu.com", "http://www.qq.com", "http://www.python.org", "http://www.sina.com", "http://www.oldboyedu.com", ] for u in urls: obj = pool.submit(get_data, u) obj.add_done_callback(parser) print("提交完成")
进程池异步回调
import os import requests from concurrent.futures import ProcessPoolExecutor pool = ProcessPoolExecutor() def get_data(url): response = requests.get(url) print("%s下载完成" % os.getpid()) return url,response.text def parser(obj): url, data = obj.result() print("%s 的长度%s" %(url, len(data))) print("%s解析完成" % os.getpid()) urls = [ "http://www.baidu.com", "http://www.qq.com", "http://www.python.org", "http://www.sina.com", "http://www.oldboyedu.com", ] if __name__ == '__main__': for u in urls: obj = pool.submit(get_data, u) obj.add_done_callback(parser) print("提交完成") print("父进程pid:", os.getpid())