##内容回顾
上周内容: # 多道技术 ### 空间复用 同一时间在内存中同时加载多个程序数据,其内存之间相互隔离 #### 时间复用 切换+保存状态 切换的两种情况: 1.一个进程遇到了IO操作时,切换到另一个进程, 2.时间片用完后,也会被强行切换 多道技术的出现使计算机可以并发执行任务 # 进程: 正在运行的程序 ,是一个资源单位, 包含程序运行的所有资源 ##### 为什么使用进程: 为了并发的执行多个任务,例如TCP中客户端的并发处理 ##### 两种使用方式 创建Process实例 继承Process类 注意:开启进程的代码必须放在 判断下面 , 因为windows平台开启子进程时,会导入代码执行一遍 来回去要执行的任务 ##### 守护进程: 被守护进程结束时守护进程也会随之结束 ##### 常用属性和方法 join 提高子进程的优先级 使得子进程先于父进程执行 父进程需要等待子进程完成后才能继续执行 daemon 设置为守护进程 is_alive 是否存活 pid 进程id terminate 终止进程 exitcode 获取进程的退出码 name 名字 ##### 僵尸和孤儿 孤儿 父进程先于子进程结束了,子进程会被操作系统接管 僵尸 在linux有一个机制,可以保证父进程在任何时候都可以访问到子进程的一些信息,所以子进程结束后并不会立即清除所有数据 ,这时候就是僵尸进程 僵尸进程会占用一些系统资源,需要父进程调用waitpid来进行清理, python会自动清理僵尸进程 ##### IPC 进程间通讯 因为每个进程之间内存是物理隔离,很多时候我们需要将数据讲给另外一个进程 ,例如:美团要把订单信息交给支付宝 1.共享文件 特点:数据量没什么限制,但是读写速度慢 2.共享内存区域 特点:数据量不能太大,速度快 3.管道 单向通讯,传输的是二进制 4.socket 编程较复杂 主要方式:共享内存 1.Manager 提供一系列常用的数据结构,但是没有处理锁的问题 2.进程Queue 是一种特殊的容器,先进先出,并且进程队列支持IPC 已经处理好锁了 # 互斥锁 相互排斥的锁,mutex 锁是什么: 本质就是一个标志,可以限制代码是否能够执行 为什么需要锁:多个进程要操作同一个资源时,可能造成数据错乱 加锁会导致并发变成串行,降低了效率,保证了数据安全 ##### 与join的区别 join会使得整个进程代码全部串行,并且主进程也无法继续执行 锁可以控制部分代码串行,其余任然并发,效率比join高 锁的粒度越小效率越高 ## 消费者生产者模型 要解决的问题,生产者用户消费者处理能力不平衡, 如果串行执行任务 效率低下 解决的方案: 1.将生产者与消费者节考耦合 2.将双方并发执行 3.提供一个共享的容器 4.生产者将数据放入容器 5.消费者从容器获取数据来处理 是否可以使用多线程来完成生产者消费者模型 必须可以 在线程中需不需要使用队列呢?? 建议使用 ##### 抢票案例: 数据出了问题,一张票 卖给了多个人 ,原因就是因为并发了 加锁解决: 将并发修改的代码加锁变成串行 # 多线程 ##### 线程是: CPU最小的执行单位 ,(操作系统最小调度运算单位),是一个固定执行过程的总称 一个进程至少包含一个线程,称之为主线程 ,是由操作系统自动开启的 运行过程中自己开启的线程 称之为子线程 线程间没有父子关系 , 例如a-b-c b和c都是a的子线程 ##### 线程对比进程: 开启速度快,开销小 同一个进程中所有线程数据共享 ##### 守护线程: 守护线程会在所有非守护线程结束时随之结束,当然守护线程可能提前结束了 ##### 使用方式: 与进程一样,开线程的代码可以放任何位置 ##### 常用方法: currentthread() 获取当前的线程对象 enumerate() 获取所有运行中的线程对象 active_count 获取存活的线程数量 ## 线程队列 queue Queue 普通队列 LifoQueue 先进后出队列 模拟堆栈 PriorityQueue 优先级队列 可以比较大小的数据都能存到其中 取出时按照从小到大取出 运算符重载 可以使自定义对象支持 算术运算符 ## 线程锁 Lock 互斥锁 Rlock 递归锁 同一个线程可以多次锁定或解锁 ,锁了几次就解几次 semaphore 信号量 可以限制同一时间多少线程可以并发执行 死锁问题 当一个资源的访问,需要具备多把锁时,然而不同的锁被不同线程持有了,陷入相互等待中 1.,尽量使用一个锁 , 设置超时 释放手里的锁, 2.抢锁时 按顺序抢 ## GIL锁 GIL全程 全局解释器锁 ,是一把互斥锁,是非常重要的,为了防止多个本地线程同一时间执行python的字节码, 因为Cpython的内存管理不是线程安全的(非线程安全的),越来越多的特性依赖于这把锁, 如果去掉这个锁的话, 会有很代码需要重构, 并且需要程序自己来处理很多的安全问题,这是非常复杂的 ##### 造成的问题 cpython多个线程不能并行,丧失了多核优势 ##### 带来的好处 保证了线程安全, ##### 如何避免带来的性能影响 首先判断任务的类型 ,分IO密集型 计算密集型 是IO密集型任务,使用多线程即可,由于大部分时间消耗在IO等待上了,所以影响不大 计算密集型,只能开启多进程 ##### 与自定义锁的区别 GIL只能 保证解释器级别数据安全,如果我们自己开启了一些不属于解释器的资源例如文件. 必须自己加锁来处理 ##### 加锁和释放 拿到解释器要执行代码时立即加锁 遇到IO时解锁 CPU时间片用完了 注意解释器的超时时间 与CPU的超时时间不同 为100nm ###进程池 线程池 池即容器 线程池 即存储线程的容器 为什么使用线程池 1.可以限制线程 数量 通过压力测试 来得出最大数量 2.可以管理线程的创建以及销毁 3.负责任务的分配 使用 创建池 submit提交任务 异步任务 将返回future对象 调用add_done_callback 可以添加回调函数 在任务结束时还会自动调用 回调函数并传入 future本身, 调用result()可以拿到任务的结果 不常用的两种方式 shutdown 可以关闭线程池,会阻塞直到所有任务全部完成 直接调用result 如果任务没有完成会进入阻塞状态 ## 异步同步 同步: 任务提交后必须原地等待任务执行结,才能继续执行 异步: 提交任务后可以立即执行后续代码 异步效率高 如何实现异步: 多线程,多进程 同步异步指的是 任务的执行方式 ## 异步回调 异步回调 本质就是一个普通函数,该函数会在任务执行完成后自动被调用 线程池, 谁有空谁处理 进程池,都是在父进程中回调 ## 协程 协程本质是在单线程下实现并发 协程是一种轻量级线程,也称为微线程,可以由应用程序自己来控制调度 好处: 可以再一个任务遇到IO操作时,自主切换到自己进程中其他线程 如果任务足够多,就可以充分利用CPU的时间片 缺点: 仅适用于IO密集型任务 ,计算密集型,如果是单线程话的串行效率更高 ,建议使用多进程来处理 当单个任务耗时较长时 协程效率反而不高 我们目的就是尽可能的提高效率 进程 线程 协程 可以使 多进程 + 线程 + 协程 UWSGI 协程对比多线程: 线程池可以解决一定的并发数量,但是如果并发量超过了机器能承受最大限制,线程池就出现瓶颈了 ## 协程的使用 gevent 需要自己安装 1.先打补丁 (本质是将原本阻塞的代码替换成非阻塞的代码) 2.gevent.spawn(任务) 来提交任务 3.必须保证主线不会结束 使用join 或是join all
#基于协程的套接字
---------------------------服务器----------------------------------------------------------------- from gevent import monkey import gevent monkey.patch_all() import socket import traceback server = socket.socket() server.bind(("127.0.0.1",1688)) server.listen(5) #最大半连接数量 三次握手未完成 可能是服务器太忙了 可能是恶意攻击 def task(client): while True: try: data = client.recv(2048) if not data: client.close() break client.send(data.upper()) except Exception as e: traceback.print_exc() client.close() break def runserver(): while True: client,addr = server.accept() gevent.spawn(task,client) gevent.spawn(runserver).join() """ 注意: join还会使得程序变成串行 所以在使用协程要注意 不是每个任务都要join 只要找一个能让主线 不会结束的任务join即可 或者主线 本来就不会结束 那就不需要join了 """ # try: # 1/0 # except Exception as e: # traceback.print_exc() # 打印异常追踪信息 # print("error",e) ---------------------------客户端---------------------------------------------------------------- import socket client = socket.socket() client.connect(("127.0.0.1",1688)) while True: msg = input("msg:") if not msg:continue client.send(msg.encode("utf-8")) print(client.recv(2048).decode("utf-8"))
## IO模型
#1、五种IO Model: * blocking IO * nonblocking IO * IO multiplexing * signal driven IO * asynchronous IO 由signal driven IO(信号驱动IO)在实际中并不常用,所以主要介绍其余四种IO Model。 #2、IO发生时涉及的对象和步骤。对于一个network IO (这里我们以read举例),它会涉及到两个系统对象,一个是调用这个IO的process (or thread),另一个就是系统内核(kernel)。当一个read操作发生时,该操作会经历两个阶段: 1)等待数据准备 (Waiting for the data to be ready) 2)将数据从内核拷贝到进程中(Copying the data from the kernel to the process) 记住这两点很重要,因为这些IO模型的区别就是在两个阶段上各有不同的情况。 #3、补充: #1、输入操作:read、readv、recv、recvfrom、recvmsg共5个函数,如果会阻塞状态,则会经理wait data和copy data两个阶段,如果设置为非阻塞则在wait 不到data时抛出异常 #2、输出操作:write、writev、send、sendto、sendmsg共5个函数,在发送缓冲区满了会阻塞在原地,如果设置为非阻塞,则会抛出异常 #3、接收外来链接:accept,与输入操作类似 #4、发起外出链接:connect,与输出操作类似
##阻塞IO模型
#1、模型就是解决某个问题的套路 IO问题: 输入输出 我要一个用户名用来执行登陆操作,问题用户名需要用户输入,输入需要耗时, 如果输入没有完成,后续逻辑无法继续,所以默认的处理方式就是 等 将当前进程阻塞住,切换至其他进程执行,等到按下回车键,拿到了一个用户名,再唤醒刚才的进程,将状态调整为就绪态 以上处理方案 就称之为阻塞IO模型 #2、存在的问题: 几乎所有的IO接口 ( 包括socket接口 ) 都是阻塞型的。这给网络编程带来了一个很大的问题,如在调用recv(1024)的同时,线程将被阻塞,在此期间,线程将无法执行任何运算或响应任何的网络请求。 #3、解决方案: 3-1、一个简单的解决方案:多线程或多进程, 在服务器端使用多线程(或多进程)。多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一个连接的阻塞都不会影响其他的连接。 #该方案的问题是: 当客户端并发量非常大的时候,服务器可能就无法开启新的线程或进程,如果不对数量加以限制 服务器就崩溃了 3-2、改进方案:线程池或进程池 “线程池”旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务。“连接池”维持连接的缓存池,尽量重用已有的连接、减少创建和关闭连接的频率。这两种技术都可以很好的降低系统开销,都被广泛应用很多大型系统,如websphere、tomcat和各种数据库等。 #改进后方案其实也存在着问题: #“线程池”和“连接池”技术也只是在一定程度上缓解了频繁调用IO接口带来的资源占用。而且,所谓“池”始终有其上限,当请求大大超过上限时,“池”构成的系统对外界的响应并不比没有池的时候效果好多少。所以使用“池”必须考虑其面临的响应规模,并根据响应规模调整“池”的大小。 3-3、协程: 使用一个线程处理所有客户端,当一个客户端处于阻塞状态时可以切换至其他客户端任务 3-4、总结 对应上例中的所面临的可能同时出现的上千甚至上万次的客户端请求,“线程池”或“连接池”或许可以缓解部分压力,但是不能解决所有问题。总之,多线程模型可以方便高效的解决小规模的服务请求,但面对大规模的服务请求,多线程模型也会遇到瓶颈,可以用非阻塞接口来尝试解决这个问题。
默认情况下所有的socket都是blocking
网络IO中必经的两个阶段
##非阻塞IO模型(non-blocking IO)
阻塞IO模型在执行recv 和 accept 时 都需要经历wait_data 非阻塞IO即 在执行recv 和accept时 不会阻塞 可以继续往下执行 #如何使用: 将server的blocking设置为False 即设置非阻塞 #存在的问题 : 这样一来 你的进程 效率 非常高 没有任何的阻塞 很多情况下 并没有数据需要处理,但是我们的进程也需要不停的询问操作系统 会导致CPU占用过高 而且是无意义的占用 #总结 我们不能否则其优点:能够在等待任务完成的时间里干其他活了(包括提交其他任务,也就是 “后台” 可以有多个任务在“”同时“”执行)。 但是也难掩其缺点: #1. 循环调用recv()将大幅度推高CPU占用率;在低配主机下极容易出现卡机情况 #2. 任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。 #案例 -------------------------------服务器.py--------------------------------- “”“ 步骤分析: 1、基于tcp的套接字链接配置 2、server.setblocking(False) # 默认为阻塞 设置为False 表示非阻塞 3、client,addr = server.accept() # 接受三次握手信息 是不会卡住的,因为非阻塞,他的缺点也是忙轮询,不停的询问,造成了无用的CPU占用 4、要从操作系统缓存中拿数据进行处理,但是还没数据,还没有客户端连过来,就会报错,所以用try .(链接客户端代码).except.(收发数据)..... 5、又因为非阻塞,send不会阻塞,但是没数据会报错,所以不能直接进行收发,要先把要连接的客户端存储到一个列表中clients 6、在对cilents进行循环处理所有客户端对象for c in clients[:]: 7、遍历的客户端对象要收数据,可能还没发,所以也要进行try...except BlockingIOError as e except ConnectionResetError:断开后关闭和删除客户端clients.remove(c) 8、如果data收到为空,则关闭和删除客户端 9、如果碰到缓冲区满了,直接发的话导致数据丢失,应该要单独把send拉出来单独发送,要把要发送的数据存到一个列表中,元组包含client,data 添加到列表msgs中 ”“” import socket import time server = socket.socket() server.bind(("192.168.13.103",1688)) server.listen() server.setblocking(False) # 默认为阻塞 设置为False 表示非阻塞 # 用来存储客户端的列表 clients = [] # 用来存储需要发送的数据和客户端对象 msgs = [] # 链接客户端的循环 while True: try: client,addr = server.accept() # 接受三次握手信息 # print("来了一个客户端了.... %s" % addr[1]) # 有人链接成功了 clients.append(client) except BlockingIOError as e: # print("还没有人连过来.....") time.sleep(0.01) # 收数据的操作,这里要注意的是不能对正在循环的列表做删除操作,所以clients[:]得到和clients一样的列表进行循环,然后可对clients仅从删除操作 for c in clients[:]: try: # 可能这个客户端还没有数据过来 # 开始通讯任务 data = c.recv(2048) if not data: c.close() clients.remove(c) #c.send(data.upper()) # 如果碰巧缓存区满了 这个数据就丢失了 # 由于此处捕获了异常 所以应该单独来处理发送数据 msgs.append((c,data)) except BlockingIOError as e: print("这个客户端还不需要处理.....",) except ConnectionResetError: # 断开后删除这个客户端 c.close() clients.remove(c) # 发送数据的操作 for i in msgs[:]: try: c,msg = i c.send(msg.upper()) msgs.remove(i) # 如果发送成功! 删除这个数据 except BlockingIOError: pass # print("over") --------------------------------客户端-------------------------------------------------- import os import socket client = socket.socket() client.connect(("127.0.0.1",1688)) while True: msg = input("msg:") if not msg:continue client.send(msg.encode("utf-8")) print(client.recv(2048).decode("utf-8"))
非阻塞IO模型
##多路复用IO模型(IO multiplexing)
#1、什么是多路复用? 多路指的是多个socket对象 一个socket就是一个传输通道 复用意思是指使用同一线程处理所有socket #2、其原理: 在非阻塞IO模型中我们需要自己不断的询问操作系统是否有数据需要处理,造成了资源浪费 多路复用使用select来检测是否有socket可以被处理(可读或者可写),我们只需要等待select的检测结果 #3、优缺点 该模型的优点: #相比其他模型,使用select() 的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多 CPU,同时能够为多客户端提供服务。如果试图建立一个简单的事件驱动的服务器程序,这个模型有一定的参考价值。 该模型的缺点: #select最多能检测1024个socket 超出直接报错 这是select自身设计的问题 最终的解决方案epoll ##案例1 ------------------------------服务器----------------------------------------------------------- “”“ 分析步骤: 1、基于tcp套接字 2、rlist 将需要检测(是否可读==recv)的socket对象放到该列表中,rlist = [server,] accept也是一个读数据操作,默认也会阻塞 也需要让select来检测 wlist 将需要检测(是否可写==send)的socket对象放到该列表中,只要缓冲区不满都可以写 3、readable_list,writeable_list,_ = select.select(rlist,wlist,[])会阻塞等到 有一个或多个socket 可以被处理,readable_list 中存储的是已经可以读取数据的socket对象 可能是服务器 可能是客户端 4、可以打印查看print(readable_list,writeable_list)---readable_list有一个server,writeable_list为空 5、处理可读列表 for soc in readable_list:处理可读列表,判断是服务器server还是客户端,服务器的话,进行client,addr = server.accept()获得客户端对象,然后rlist.append(client)将新连接的socket对象 加入到待检测列表中 客户端的话要进行收发数据,data = soc.recv(2048)判断data是否为空 soc.close(),rlist.remove(soc)如果对方下线 关闭socket 并且从待检测列表中删除 6、soc.send(data.upper())直接发送的话是有问题的 ,不能直接发 因为此时缓冲区可能已经满了 导致send阻塞住, 所以要发送数据前一个先这个socket交给select来检查,wlist.append(soc)放到wlist检测列表中先保存起来 7、msgs列表 --存储需要发送的数据 等待select 检测后 在进行发送msgs.append((soc,data)) 8、处理可写列表for soc in writeable_list:该列表全是可写列表 不需要判断,由于一个客户端可能有多个数据要发送 所以遍历所有客户端for i in msgs[:], if i[0] == soc ,soc.send(i[1])发送成功 将这个数据从列表中删除 msgs.remove(i) 数据已经都发给客户端 这个socket还需不需要检测是否可写,必须要删除,否则 只要缓冲区不满 一直处于可写 导致死循环wlist.remove(soc) 9、收发数据添加try.....except ConnectionResetError: soc.close() rlist.remove(soc) wlist.remove(soc)对方下线后 应该从待检测列表中删除 socket 10、可写有个问题 :只要缓冲区不满都可以写 wlist.remove(soc) # 否则 只要缓冲区不满 一直处于可写 导致死循环 删除socket两个地方:发完数据和对方强行下线 ”“” import socket import select server = socket.socket() server.bind(("127.0.0.1",1688)) server.listen() # server.setblocking(False) rlist = [server,] # 将需要检测(是否可读==recv)的socket对象放到该列表中 # accept也是一个读数据操作,默认也会阻塞 也需要让select来检测 # 注意 select最多能检测1024个socket 超出直接报错 这是select自身设计的问题 最终的解决方案epoll wlist = [] # 将需要检测(是否可写==send)的socket对象放到该列表中 # 只要缓冲区不满都可以写 msgs = [("socket","msg")] # 存储需要发送的数据 等待select 检测后 在进行发送 print("start") while True: readable_list,writeable_list,_ = select.select(rlist,wlist,[]) # 会阻塞等到 有一个或多个socket 可以被处理 print("%s个socket可读" % len(readable_list),"%s个socket可写" % len(writeable_list)) """ readable_list 中存储的是已经可以读取数据的socket对象 可能是服务器 可能是客户端 """ # 处理可读列表 for soc in readable_list: if soc == server: # 服务器的处理 client,addr = server.accept() #将新连接的socket对象 加入到待检测列表中 rlist.append(client) else: try: # 客户端的处理 data = soc.recv(2048) if not data: soc.close() rlist.remove(soc) # 如果对方下线 关闭socket 并且从待检测列表中删除 continue # 不能直接发 因为此时缓冲区可能已经满了 导致send阻塞住, 所以要发送数据前一个先这个socket交给select来检查 # soc.send(data.upper()) if soc not in wlist: wlist.append(soc) # 将要发送的数据先存起来 msgs.append((soc,data)) except ConnectionResetError: soc.close() # 对方下线后 应该从待检测列表中删除 socket rlist.remove(soc) wlist.remove(soc) # 处理可写列表 for soc in writeable_list: # 由于一个客户端可能有多个数据要发送 所以遍历所有客户端 for i in msgs[:]: if i[0] == soc: soc.send(i[1]) # 发送成功 将这个数据从列表中删除 msgs.remove(i) # 数据已经都发给客户端 这个socket还需不需要检测是否可写,必须要删除 wlist.remove(soc) # 否则 只要缓冲区不满 一直处于可写 导致死循环 print("over") ------------------------------客户端----------------------------------------------------------- import os import socket client = socket.socket() client.connect(("127.0.0.1",1688)) while True: msg = input("msg:") if not msg:continue client.send(msg.encode("utf-8")) print(client.recv(2048).decode("utf-8")) ##案例2:该案例只是总结思路 import socket import time import select server = socket.socket() server.bind(("127.0.0.1",1688)) server.listen() # server.setblocking(False) # 默认为阻塞 设置为False 表示非阻塞 """ 参数1 rlist 里面存储需要被检测是否可读(是否可以执行recv)的socket对象 参数2 wlist 里面存储需要被检测是否可写(是否可以执行send)的socket对象 参数3 xlist 存储你需要关注异常条件 忽略即可 参数4 timeout 检测超时时间 一段时间后还是没有可以被处理的socket 那就返回空列表 返回值: 三个列表 1 已经有数据到达的socket对象 2 可以发送数据的socket对象 怎么可以发 缓冲区没有满 3 忽略.... """ rlist = [server,] wlist = [] # 要发送的数据和socket msgs = [] while True: ras,was,_ = select.select(rlist,wlist,[]) # 阻塞直到socket可读或是可写 # 处理可读的socket for s in ras: if s == server: client,addr = server.accept() rlist.append(client) else: try: # 收数据 data = s.recv(2048) if not data:
##这里可以简化代码 利用raise抛异常,except ConnectionResetError:会捕获到 raise ConnectionResetError() wlist.append(s) # s.send(data.upper()) # 将要发送的数据和socket 保存起来
##以后这里就是给客户端返回的数据逻辑处理,这里只是为了演示,返回大写给客户端
msgs.append((s,data)) except ConnectionResetError: s.close() rlist.remove(s) if s in wlist:wlist.remove(s) # 处理可写的socket for s in was: for msg in msgs[:]: if msg[0] == s: s.send(msg[1].upper()) # 发送成功之后 删除已经无用的数据 并且需要将socket从wlist列表中删除 # 不删除会造成死循环 因为socket 一直处于可写状态 msgs.remove(msg) wlist.remove(s)
多路复用基本原理就是select/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。它的流程如图: