并发编程
多道技术:
空间复用:
同一时间在内存中加载不同的数据,其内存之间是相互隔离。
时间上的复用:切换加保存。
切换的两种情况:1一个进程遇到IO操作会切换到另一个进程
2.时间片用完了也会被强行切换,切换的时候会记录状态。
多道技术的出现使计算机由串行执行任务变成并发执行任务。
进程:
进程就是一个运行的程序,一个程序运行可以产生多个进程,但进程与进程之间是相互隔离的,
它也是一个资源单位,包含运行程序所需要的所有资源。
为什么使用进程?
为了并发的执行多个任务。
两种使用方式:
创建Process实例
继承Process类,覆盖run方法
注意:开启进程的代码必须放在判断下面,因为windows开启子进程时,会导入代码执行一遍来回去要执行的任务。
守护进程:
在被守护进程结束时,守护进程也会随之结束。当然守护进程也可以提前结束。
常用属性和方法:
join 提高子进程的优先级,让子进程先于父进程执行代码,父进程的代码必须等被join的子进程的代码执行完才会执行。
is_alive 进程是否存活
pid 查看进程id
terminate 终止进程
exitcode 获取进程的退出码
name 查看进程的名字
daemon 设置为守护进程
僵尸和孤儿进程:
孤儿 父进程先于子进程结束,子进程会被操作系统接管,孤儿进程有其存在的意义。
僵尸进程 :在linux中有一个机制,可以保证父进程在任何时候都可以访问到子进程的一些信息,所以子进程结束后并不会立即清除所有数据,这时候就是僵尸进程。
僵尸进程会占用一些系统资源,需要父进程调用waitpid来进行清理,python会自动清理僵尸进程。
IPC
进程间通讯
因为每个进程之间内存是物理隔离的,很多时候我们需要将数据讲给另外一个进程,例如:美团要将支付信息交给支付宝
1.共享文件
特点:数据量没有什么限制,但读写速度慢
2.共享内存区域
特点:数据量小,但读写速度快
3.管道
特点:只能单向通讯。而且要二进制解码
4.socket
特点:代码结构复杂
主要方式:共享内存:
1.Manager 提供一系列常用的数据结构,但是没有处理锁的问题
2.进程Queue 是一种特殊的容器,队列,规定了先进先出,并且支持IPC,已经处理好了锁了。
互斥锁:
相互排斥的锁 mutex
其本质就是一个标志,不是说锁住了代码,只是限制了代码是否能够执行
为什么需要锁:因为多个进程使用同一个资源时会造成数据错乱
特点:加锁会使进程由并发变成串行,提高了安全性但降低了效率。
与join的区别:
join是把整个进程代码全部变为串行,这样失去了并发的本意,而且限制了主进程的代码执行
锁是想锁哪里就锁哪里,锁的部分变成的串行,其他仍然并发,且不限制主进程代码的执行。
锁的粒度越小效率越高。
消费者生产者模型:
要解决的问题:生产者与消费者处理能力不平衡,如果串行执行任务,效率极低。
解决的方案:
1.将生产者与消费者分开耦合
2.将双方并发执行
3.提供一个共享的容器
4.生产者将数据放入容器
5.消费者从容器中取数据
多线程:
线程:cpu最小的执行单位,操作系统最小的调度单位,一个固定的执行流程的总称。
一个进程至少包含一个线程,称之为主线程,是由操作系统自动开启的。
运行过程中自己开启的线程称为子线程
线程之间是平等的,没有父子之分,而且线程的开启代码可以放在任何位置,不需要放在判断下面。
特点:线程的创建开销比进程小。2.同一个进程内的多个线程可以共享同一个进程内的资源。不同进程的线程也是隔离的。
使用方式:和进程的使用方式一样,只是代码可以放到任意位置
常用方法:
currentthread()获取当前的线程对象
active_cournt 获取存活的线程个数
enumerate()获取所有运行中的线程对象。
线程队列:
queue:
Queue 普通队列,作用和joinablequeue一样,但不能作为IPC使用
LifoQueue 先进后出,后进先出,堆栈
priorityQueue 优先级队列,里面必须是可以被运算的,打印出来,越小的优先级越高
线程锁:
lock互斥锁
RLock递归锁,同一个线程可以多次加锁,但还是要按照规定几次加锁对应几次解锁
信号量 semaphore 可以限制同一时间多少线程可以同时并发执行
死锁:当一个资源的访问,需要具备多把锁时,然而不同的锁被不同线程持有了,陷入相互等待中。
1.尽量使用一个锁,设置超时释放手里的锁
2.抢锁时,按照顺序抢
GIL互斥锁:
全局解释器锁,是锁解释器的,因为cpython的内存管理是非线程安全的,它本质上也是一个互斥锁,为了防止多个本地线程同时执行python的字节码。
有了这把锁,线程要执行代码必须先抢锁,谁抢到就谁先执行。
优点:解决了cpython的内存管理的线程安全。
缺点:多个线程不能并行执行,失去了多核的优势。
为什么不处理它,因为去掉这个锁的话,会有很多代码需要重构,并且需要程序自己来处理很多的安全问题,这样成本太大,而且很复杂、
如何避免性能影响?
首先判断任务是IO密集型还是计算密集型
IO密集型使用多线程
计算密集型使用多进程
它与自定义锁的区别:
它只保证解释器级别的数据安全,比如引用计数,如果我们自己开启了一些不属于解释器的资源,比如共享文件,那么还是需要我们自己加锁来保证安全。
加锁和释放:
拿到解释器要执行的时候立即加锁
遇到IO时解锁
cpu时间片用完了,注意解释器的超时时间与cpu的超时时间不同,为100nm(纳秒)
进程池和线程池:
池:就是一个容器
线程池:就是存储线程的容器
为什么使用线程池:
1.可以限制线程数量 通过压力测试来得出最大数量
2.可以管理线程的创建和销毁
3.可以负责任务的分配
进程池一样
使用:创建池,然后submit提交任务
异步任务将返回future对象,调用add_done_callback可以添加回调函数
在任务结束时还会自动调用回调函数并传入future本身,调用result()可以拿到任务的结果。
不常用的两种方式
shutdown 可以关闭线程池,会阻塞直到所有任务全部完成
直接调用result 如果任务没有完成会进入阻塞状态、
异步和同步:
同步是指提交任务后,必须等待任务执行完后的结果才能继续执行
异步是指提交任务后,不需要等待任务执行完毕,可以去做其他事情
异步回调:
本质上就是一个普通函数,该函数会在任务执行完成后自动被调用
线程池,谁有空谁执行
进程池,都是在父进程中回调
协程:
单线程实现并发,协程也称轻量级线程,也称微线程,可以由应用程序自己来控制调度。
好处:可以在一个任务遇到IO操作时,自主切换到自己进程中其他线程
如果任务足够多的,就可以充分利用cpu 的时间片
缺点:
仅适用于IO密集型任务,计算密集型,如果是单线程下的串行效率更高,建议使用多进程来处理
当单个任务耗时较长时,协程效率反而不高
我们的目的就是尽可能的提高效率
进程 线程 协程
可以多进程 +线程+协程
协程对比多线程:
gevent 需要自己安装
1.先打补丁 (本质是将原本阻塞的代码替换成非阻塞的代码)
2.gevent.spawn(任务) 来提交任务
3.必须保证主线不会结束 使用join 或是join all
IO模型:
IO问题:
输入输出
我要一个用户名用来执行登陆操作,问题用户名需要用户输入,输入需要耗时, 如果输入没有完成,后续逻辑无法继续,所以默认的处理方式就是 等
将当前进程阻塞住,切换至其他进程执行,等到按下回车键,拿到了一个用户名,再唤醒刚才的进程,将状态调整为就绪态
以上处理方案 就称之为阻塞IO模型
存在的问题:
当执行到recv时,如果对象并没有发送数据,程序阻塞了,无法执行其他任务
解决方案:
多线程或多进程,
当客户端并发量非常大的时候,服务器可能就无法开启新的线程或进程,如果不对数量加以限制 服务器就崩溃了
线程池或进程池
首先限制了数量 保证服务器正常运行,但是问题是,如果客户端都处于阻塞状态,这些线程也阻塞了
协程:
使用一个线程处理所有客户端,当一个客户端处于阻塞状态时可以切换至其他客户端任务
非阻塞IO模型
非阻塞IO即 在执行recv 和accept时 不会阻塞 可以继续往下执行
如何使用:
将server的blocking设置为False 即设置非阻塞
存在的问题 :
这样一来 你的进程 效率 非常高 没有任何的阻塞
很多情况下 并没有数据需要处理,但是我们的进程也需要不停的询问操作系统 会导致CPU占用过高
而且是无意义的占用
import socket import time server = socket.socket() server.bind(("192.168.13.103",1688)) server.listen() server.setblocking(False) # 默认为阻塞 设置为False 表示非阻塞 # 用来存储客户端的列表 clients = [] # 链接客户端的循环 while True: try: client,addr = server.accept() # 接受三次握手信息 # print("来了一个客户端了.... %s" % addr[1]) # 有人链接成功了 clients.append(client) except BlockingIOError as e: # print("还没有人连过来.....") # time.sleep(0.5) # 服务你的客人去 for c in clients[:]: try: # 可能这个客户端还没有数据过来 # 开始通讯任务 data = c.recv(2048) c.send(data.upper()) except BlockingIOError as e: print("这个客户端还不需要处理.....",) except ConnectionResetError: # 断开后删除这个客户端 clients.remove(c) print("=======================",len(clients))
多路复用
假设原本有30个socket 需要我们自己来处理, 如果是非阻塞IO模型,相当于从头开始问道尾,如果没有需要处理的
回过头来再次重复,
多路复用解决问题的思路,找一个代理即select,将你的socket交给select来检测
select 会返回 那些已经准备好的 可读或者可写的socket
我们拿着这些准备好的socket 直接处理即可
对比线程池
避免了开启线程的资源消耗
缺点:
同时检测socket不能超过1024
异步IO
阻塞IO recv accept 会将当前线程阻塞住 同步
非阻塞IO recv accept 不会阻塞当前线程 ,没有数据直接抛出异常
分析 属于同步还是异步?
recv (wait_data,copy_data) 设置为非阻塞之后 wait_data不会再阻塞
但是copy_data 也是IO操作 还是会阻塞
也属于同步
多路复用 也属于同步IO
同步 异步 任务的执行方式
同步IO 执行IO任务的方式
异步IO
异步IO
线程池中的submit 就是异步任务
异步的特点就是 立马就会返回
同步翻译为sync 异步async