通过学习bi站 蚂蚁学Python
老师视频总结文档,仅用于学习。。。
python进程、线程、协程
-
多线程:threading,利用CPU和IO可以同时执行的原理,不会让CPU干巴巴的等待IO完成
-
多进程:multiprocessing,利用多核CPU的能力,真正的并行执行任务
-
异步IO:asyncio,在单线程利用CPU和IO同时执行的原理,实现函数异步执行
方法:
-
使用Lock对资源加锁,防止冲突访问(锁起来就可以实现有序访问)
-
使用Queue实现不同线程/进程之间的数据通信,实现生产者—消费者模式
-
使用线程池Pool/进程池Pool,简化线程/进程的任务提交、等待结束、获取结果
-
使用subprocess 启动外部程序的进程,并进行输入输出交互
1. CPU密集型计算、IO密集型计算
1.1 CPU密集型(CPU-bound)
CPU密集型也叫计算密集型,是指在I/O在很短的时间就可以完成,CPU需要大量的计算和处理,特点是CPU占用率相当高
例如:
- 压缩和解压缩
- 加密解密
- 正则表达式搜索
1.2 IO密集型(I/O-bound)
IO密集型是指是,系统运作大部分的状况是CPU在等I/O (硬盘/内存) 的读/写操作,CPU占用率仍然较低
例如:
- 文件处理程序(大量读写程序)
- 网络爬虫程序(大量网络下载,网络下载占用很长时间)
- 读写数据库程序(网络的读取,网络开销比较多)
2. 多进程、多线程、多协程的对比
2.1 多进程(multiprocessing)
-
优点:可以利用多核CPU并行运算
-
缺点:占用资源最多、可启动数目比线程少
-
适用于:CPU密集型计算
-
一个进程可以启动N个线程
2.2 多线程(threading)
-
优点:相比进程,更轻量级、占用资源少
-
缺点:
- 相比进程,多线程只能并发执行,并不能利用多CPU(GIL)
- 相比协程,启动数目有限制,占用内存资源,有线程切换开销
-
适用于:IO 密集型计算、同时运行的任务数目要求不多
-
一个线程可以启动N个协程
2.3 多协程(Coroutine -> asyncio)
- 优点:内存开销最少、启动协程数量最多
- 缺点:支持的库有限制、代码实现复杂
- 适用于:IO密集型计算、需要超多任务运行、但有现成库支持的场景
3. Python 的GIL
python慢的原因
- 动态类语言,边解释边执行
- GIL的存在导致python无法利用多核CPU并发执行
3.1 GIL是什么
全局解释器锁(Global Interpreter Lock)
是计算机程序设计语言解释器用于 同步线程的一种机制,它使得任何时刻仅有一个线程在执行
即便在多核处理器上,使用GIL的解释器也只允许同一时间执行一个线程。
即使电脑有多核CPU单个时刻也只能使用一个,相比较于并发加速的C++/Java慢很多
3.2 GIL 存在的原因
Python设计初期,为了规避并发问题引入了GIL,现在想去除却去除不掉了
引入GIL的原因:
-
为了解决线程之间数据完整性和状态同步问题
-
Python中对象的管理,是使用引用计数器进行的,引用数为0则释放对象
-
例:线程A和线程B都引用的对象obj,obj.ref_num = 2,假如线程A和线程B都想撤销对obj的引用
-
- GIL的好处:简化了python对共享资源的管理
3.3 规避GIL带来的限制
1、多线程
threading
机制 还是有用的,用于IO密集型计算
因为在
I/O
(read、write、send、recv、etc.)期间,线程会释放GIL
, 实现CPU和IO并行,因此多线程用于IO密集型计算,依然可以大幅度提升速度
但是,多线程用于CPU密集型计算,只会拖慢速度
2、使用
multiprocessing
的多进程机制实现并行计算、利用多核CPU优势(为了应对GIL的问题,Python提供了multiprocessing)
3.4 并发与并行
并发:
指的是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执 行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)
并行:
指的是任务数小于等于cpu核数,即任务真的是一起执行的。
4. 线程
4.1 创建多线程的方法
1、准备一个函数
def my_func(a,b)do_sum(a,b)
2、怎样创建一个线程
import threading t = threading.Thread(target=my_func, args=(100,200)) # target 表示要执行的任务(传入函数名),args表示传入函数的参数
3、启动线程
t.start()
4、等待结束
t.join()
4.2 单线程与多线程对比
import blog_spider
import threading
import time# 单线程
def single_thread():print("single_thread begin")for url in blog_spider.urls:blog_spider.craw(url)print("single_thread end")# 多线程
def multi_thread():print("multi_thread begin")threads = []for url in blog_spider.urls:threads.append(threading.Thread(target=blog_spider.craw, args=(url,)))for thread in threads:thread.start() # 开启线程for thread in threads:thread.join() # 等待线程结束print("multi_thread end")if __name__ == '__main__':start = time.time() # 开始时间single_thread()end = time.time() # 结束时间print("single thread cost:", end-start, "seconds") # single thread cost: 8.59098196029663 secondsstart = time.time()multi_thread()end = time.time()print("multi thread cost:", end - start, "seconds") # multi thread cost: 0.82523512840271 seconds
4.3 多线程生产消费模式
4.5 线程池
4.5.1 线程池的原理
线程的生命周期:
新建线程系统需要分配资源、终止线程系统需要回收资源,如果可以重用线程,则可以减去新建/终止的开销
4.5.2 使用线程池的好处
- 提升性能:因为减去了大量新建、终止线程的开销,重用了线程资源;
- 适用场景:适合处理突发性大量请求或需要大量线程完成任务、但实际任务处理时间较短
- 防御功能:能有效避免系统因为创建线程过多,而导致系统负荷过大响应变慢等问题
- 代码优势:使用线程池的语法比自己新建线程执行线程更加简洁