并发编程
并发编程概要:
计算机操作系统发展史
1.多道技术
1.空间复用
同一时间在内存中存放多个程序,内存相互隔离
2.时间复用
CPU在遇到IO时切换到另一个程序
可以实现并发
切换+保存状态
2.进程 理论 *****
进程是一个资源单位,包含了该程序运行所需的所有资源
为什么使用它? 为了提高程序的执行效率,当遇到IO阻塞或需要同时执行多个任务时
linux 和 windows创建进程的区别
linux下创建子进程,会直接将父进程的所以数据复制一份给子进程
windows创建子进程时,子进程会加载父进程的字节码(没有任何的多余符号:空格,换行)
3.进程的使用
1.实例化Process这个类,传入一个target参数
2.继承Process,实现run函数
4.IPC(进程间通讯)
为什么使用IPC? 进程间的内存是物理隔离的,无法直接访问数据
实现方式
1.共享文件 速度慢 没有锁
2.管道 单向通讯,需要有父子关系 没有锁
3.共享内存 速度快,数据量较小 Manager没有锁 Queue有锁(先进先出)
5.守护进程
A进程守护B进程 B进程结束,A也结束 皇帝死了,妃子陪葬
6.互斥锁 死锁 可重入锁 信号量 (抢票代码 死锁代码)
1.为什么需要锁?
多个进程同时读写同一份数据时,可能造成数据混乱 (本地IO速度很快,基本不会出现问题,但不代表不会)
读数据没有必要加锁,写数据必须加
锁的特点:
并发改串行
被锁的代码将会变成串行,效率降低
锁的原理:
就是加上一堆判断,锁相当于一个标记(别的代码遇到标记,不去执行)
互斥锁:相互排斥,就像在宿舍大家公用一个厕所,一个使用中,其他人不能用
同一线程不能多次acquire,会卡死
2.死锁?
死锁造成的问题.程序卡死
一个锁不会产生死锁,当有多个锁多个线程时会产生死锁
a b 锁
p k 线程
当p 和 k 都需要a和b锁时才可能产生死锁
3.可重入锁RLock
同一个线程可以多次执行acquire,执行一次acquire 计数加1
执行一次release,次数减一,执行acquire的次数需要与release的次数对应
在执行被锁的代码时,同一个线程,不会判断次数.其他线程需要判断,计数为0才可以执行
不是用来解决死锁的,可重入锁也有可能死锁
4.信号量
案列:
sem = semaphore(2)
acquire
code.....
release
开了十个线程,只能有两个同时执行
信号量作用:限制同时执行被锁代码的线程数量
7.生产者消费者模型 (思聪吃热狗代码)
while True:
生产数据 1
处理数据 10
生产数据和处理数据的能力不匹配,一个快一个慢,整体效率变低
第一个问题:学习多进程,可以将生产和处理,分到不同进程中,来解决能力不匹配的问题,快的多干点,慢的少干点
第二个问题:作为处理数据的一方不知到什么时候会有数据,两个进度不同,使用一个共享的数据容器
将生产和消费分到不同进程(线程)中,使用共享数据容器来同步数据
8.线程理论 *****
线程是CPU的基本执行单位
一连串的代码就是像流水线,cpu会按照顺序依次执行他们
为什么用线程? 需要实现并发执行任务,并发任务进程也可以完成
线程和进程的区别
1.资源开销
进程开销大,线程开销小
2.数据共享
一个进程内的所有线程数据共享
举例:
一个进程中,必然包含一条主线程
一个进程可以包含多个线程
进程是工厂,线程是流水线
一个工厂有多个流水线,(至少得有一个)所有流水线都可以使用工厂内的资源
9.线程的使用 (生产者消费者代码 抢票代码)
使用线程
1.实例化Thread类,参数target传入任务
2.继承Thread类,实现run函数
10.守护线程
守护线程:会在被守护线程结束时一并结束
与守护进程的区别:
a 守护 b 同时还有另一个线程 c
a 会等到 b 和 c都结束才结束
皇后 守护皇帝
皇宫里还有太子
皇后会等到皇帝和太子都死了,才死.
守护线程会等待所有非守护线程结束后才算结束
main
sub1
sub2
sub3
sub1.deamon = True
sub1会等待 main. sub2. sub3 全都结束,才会结束
11.GIL
问题:一个py程序,要想运行,必须运行解释器,解释器的工作是翻译代码,并执行
当一个py进程中,有多个线程,线程的任务就是执行代码,意味者多个线程都要使用解释器
简单的说:多线程会争抢解释器的执行权
如果是自己开的线程,多线程要访问相同数据,加锁就能解决,但是有一些代码不程序员(如:GC)写的,也确实需要共享使用,就是解释器
GC:垃圾回收器,负责清理内存中的无用数据,清理垃圾也需要执行代码,但是GC不应该卡住用户的代码执行,只能开线程
GC看到 x = 10 ,x = 1.准备删除10,这时候突然CPU切到用户线程 ,赋值 a = 10 ,此时还没有问题
紧接着CPU又切到GC,GC上来就删除10,再切到用户线程,a所指向的地址被清理了,就会产生错误
解决方案: 给解释器加上锁,保证GC执行期间,用户线程不能执行
全局解释器锁带来的问题:
同一时间只有一个线程能使用解释器,无法利用多核CPU
那Cpython多线程是鸡肋?
当程序是IO密集时,多线程能提高效率,IO的速度明显要比CPU执行速度慢
当程序时计算密集时,多线程无法提升效率,得使用多进程
所谓IO密集型任务,是指磁盘IO、网络IO占主要的任务,计算量很小。比如请求网页、读写文件等。当然我们在Python中可以利用sleep达到IO密集型任务的目的。
所谓计算密集型任务,是指CPU计算占主要的任务,CPU一直处于满负荷状态。比如在一个很大的列表中查找元素(当然这不合理),复杂的加减乘除等。
12.GIL和自定义锁
相同点:都是互斥锁
不同点:
GIL:解释器级别锁,锁的是解释器代码
自定义锁:锁的是自己写的代码
自动加锁:只要有线程在使用解释器 自动解锁: 1.IO 2.执行时间过长3ms 3.线程执行结束
有了GIL,为什么还需要自定义锁?
GIL不清楚什么代码会造成数据竞争问题,不知道什么地方该加
13.进程池,线程池
池是一种容器
进程池里面装的是进程
为什么需要这个容器?
当程序中有多个进程时,管理变得非常麻烦
进程池可以帮我们管理进程
1.进程的创建
2.进程的销毁
3.任务的分配
4.限制最大的进程数,保证系统正常运行
使用方式?
ThreadPoolExecutor 线程池
实例化 是指定最大线程数
ProcessPoolExecutor 进程池
实例化 是指定最大进程数
执行submit来提交任务
14.队列 queue,这个queue和进程里的Queue不同,就是一个简单的容器
队列是一种数据的容器
特点:先进先出
queue 先进先出
lifoqueue 先进后出
priorityqueue 优先级队列,整型表示优先级,数字越大优先级越低
15.同步异步 阻塞 非阻塞
概念:
同步
提交任务需要等待任务执行完成才能继续执行
异步
提交任务不需要等待任务执行,可以立即继续执行
指的是提交任务的方式
阻塞:阻塞态
遇到IO,失去了CPU执行权,看上去也是在等,与同步会混淆
非阻塞:就绪态,运行态
代码正常执行
阻塞非阻塞指的是线程状态
线程的三种状态 就绪 运行 阻塞
16.异步回调
发起了一个异步任务,任务完成后回来调用指定的函数
pool = ThreadPoolExecutor()
f = pool.submit(task)
f.add_done_callback(函数名)
17.协程 ****
协程:指的是单线程实现并发
为什么用协程? 多线程实现并发有什么问题?
TCP程序中,处理客户端的连接,需要子线程,但是子线程依然会阻塞.一旦阻塞,CPU切走,但是无法保证是否会切到当前程序.
提高效率的解决方案:是想办法尽可能多的占用CPU,当程序遇到阻塞时,切换到别的任务,如:计算,注意使用程序内切换
协程的使用:
1.生成器
2.greenlet 封装了生成器,不能检测到IO行为
3.gevent 封装了grennlet,既能够切换执行,也能检测IO
# gevent 需要配合monkey补丁,monkey补丁内部将原本阻塞的模块,替换为了非阻塞的
# monkey必须放在导入(需要检测IO的模块)模块之前
monkey.patch_all()
gevent核心函数spawn(函数名)
join让主线程等待所有任务执行完成才结束
18.IO模型
网络传输的两个阶段
waitdata(recv accept) copydata(recv accept send)
应用程序的缓存和系统缓存之间会需要copydata
recv accept 先wait,再copy
send 只有copy
阻塞IO
之前所学,除了协程,都是阻塞IO
应用程序发送系统调用,操作系统等待数据(wait),数据准备好,return data
非阻塞IO
recv,send,accept都不会阻塞,会立即执行,但是不能保证立马就有数据,没有数据抛出异常
我们需要手动捕获异常,捕获异常后可以处理别的任务
CPU的利用率提高了
但是同时也浪费了CPU,当没有任何数据处理的时候,就在不断向操作系统要数据,空转
while True:
pass
多路复用
管理连接的一种方式
为什么使用它? 相对于非阻塞IO,降低无用的系统调用
怎么管?
核心函数select,默认是阻塞的,阻塞到有任意一个连接可以被处理
创建连接和管理连接
1.创建服务器socket对象
2.将服务器对象交给select来管理
3.一旦有客户端发起连接,select将不在阻塞
4.select将返回一个可读的socket对象列表(第一次只有服务器)
5.服务器的可读代表有连接请求,需要执行accept.返回一个客户端连接conn,由于是非阻塞,不能立即去recv
6.把客户端socket对象也交给select来管理,将conn加入两个被检测的列表中
7.下一次检测到可读的socket,可能是服务器,也可能客户端,所以加上判断,是服务器就accept,是客户端就recv
8.如果检测到有可写(可以send就是系统缓存可用)的socket对象,则说明可以向客户端发送数据了
7 和 8 执行顺序不是固定的
异步IO
IO包括网络IO和本地IO,上面的三种描述的都是网络IO
不能本地IO的问题
解决的方案就是:
将同步的IO操作改成异步的IO操作,在IO期间,可以执行其他的任务
使用asyncio模块
19.socketserver
是什么? 对服务器端的socket的封装
封装了多线程和socket
为什么用? 简化代码
使用方法:
socketserver (forkingUDP forkingTCP windows无法使用)
核心类:ThreadingUDPServer,ThreadingTCPServer
ThreadingTCPServer 实例化时,传入服务器地址和自定义的一个数据处理类
自定义类需要继承BaseRequestHandler类中需包含handle函数
对象调用serve_forever
20.Event
是什么?
线程间通讯的方式
为什么用?
简化代码
set()设置为True
wait()阻塞,直到为True