网络编程与并发编程
网络编程
软件开发框架
软件开发框架可以看做是围墙,在围墙围起来的空间范围内对程序的功能进行了划分,规定程序各个功能模块之间运行的逻辑以及请求逻辑
在我们之前所编写的ATM与选课系统中,都使用了三层架构,三层架构也属于软件开发框架的一部分
常用软件开发架构
C/S架构
C即client,也就是我们常说的客户端 S即server,也就是我们都会用到的服务端 我们所使用的各个软件,也就是APP其实就是各个公司开发的客户端 通过这些客户端引导我们使用到服务端所提供的服务 优势:具有高度定制化的属性,可以根据开发公司的需求有不同的功能 劣势:只有在客户下载后才可以使用
一般情况下,客户端与服务端的交互需要使用到网络,但是如果客户端与服务端存储在同一台计算机上时就不需要了
B/S架构
B即Browser,就是我们日常使用的浏览器 S依旧是server,在这里指代服务器或服务端 浏览器可以作为所有服务端的客户端使用 所以B/S架构的本质仍是C/S架构 优势:用户的访问更加方便,无需下载 劣势:局限性很强,无法进行高度定制
服务端必备的三大要素
1.24小时全天候服务
2.固定的地址便于查找
3.可以同时为多个客户提供服务
之前的ATM与选课系统,我们所使用的第一层可以视为我们的客户端,第二层及第三层就是我们的服务端
软件设计的趋势走向会慢慢的向着统一接口发展,就像我们可以在微信与支付宝的小程序访问到其他软件的功能一样,接口逐步统一
网络编程引入
什么是网络编程?
基于网络编写代码,并利用接口实现数据远程交互
学习网络编程的目的是什么?
可以进行基于C/S或者B/S架构的程序代码编写
网络最早起源于美国的军事领域,是为了解决数据远程传递预处理的问题
网络编程必备条件
数据的远程交互(实现数据远程传递与处理)
数据的远程交互都是以必须的物理连接介质为基础的例如我们日常生活中的网线、电话线
OSI七层协议
OSI七层协议规定了所有计算机在进行远程数据交互时必须以相同流程处理数据来进行传递,相当于工厂中的流水线,在规定固定的加工流程的同时,要求必须使用同样规格的加工工具,也就是协议中所要求的计算机制造过程中必须拥有相同的功能硬件
应用层 表示层 会话层 传输层 网络层 数据链路层 物理连接层
可以采取只记忆首个文字的方式进行快速记忆
在我们现实中的使用过程中,常用到的是在七层的基础上整合出的五层或者四层
# 五层结构 应用层 传输层 网络层 数据链路层 物理连接层 # 四层结构 应用层 传输层 网络层 网络接口层
PS:接收网络消息的传递顺序是在这七层中自下而上传递
发送网络消息时与接收相反,自上而下传递
OSI七层协议分层理解
物理连接层
主要用于确保计算机之间的物理连接介质,接收数据(bytes类型、二进制)
数据链路层
1.规定了电信号的分组方式
2.以太网协议
规定了计算机在出厂的时候都必须有一块网卡,网卡上有一串数字
该数字相当于是计算机的身份证号码是独一无二的
该数字的特征:12位16进制数据
前6位产商编号,后6位流水线号
该数字也称为:以太网地址/MAC地址网络相关专业名称
计算机之间要想实现数据交互必须要'连接'到一起
1.交换机
能够将所有接入交换机的计算机彼此互联起来
2.广播
首次查找接入同一个交换机的其他计算机,需要向交换机传递一个所有计算机都可以接受到的信号
首次被查找的计算机回应查找它的计算机,并附带自己的mac地址
4.广播风暴
接入同一台交换机的多台计算机同时发广播
5.局域网
可以简单的理解为有单个交换机组成的网络
在局域网内可以直接使用mac地址通信
6.广域网
可以简单的理解为范围更大的局域网
7.互联网
由所有的局域网、广域网连接到一起形成的网络
8.路由器
不同的局域网计算机之间是无法直接实现数据交,互的需要路由器连接网络层
IP协议:规定了所有接入互联网的计算机都必须有一个IP地址 类似于身份证号
mac地址是物理地址可以看成永远无法修改
IP地址是动态分配的,不同的场所IP是不同的
IP地址特征:
IPV4:点分十进制
0.0.0.0
255.255.255.255
IPV6:能够给地球上每一粒沙分一个IP地址
IP地址可以跨局域网传输
IP地址可以用来标识全世界独一无二的一台计算机传输层
PORT协议(端口协议)
用来标识一台计算机上面的某一个应用程序
范围:0-65535
特征:动态分配(洗浴中心号码牌)
建议:
0-1024 系统默认需要使用
1024-8000 常见软件的端口号
8000之后的URL:统一资源定位符(网址)
网址本质是有IP和PORT组成的IP+PORT:能够定位全世界独一无二的一台计算机上面的某一个应用程序
域名解析:将网址解析成IP+PORT
我们使用网址的方式记忆是因为IP+PORT的记忆方式复杂繁琐,网址相对而言简单明了,便于记忆
IP:PORT 实际使用冒号连接
114.55.205.139:80TCP与UDP协议
TCP与UDP协议都是用来规定通信的方式的
采用通信协议和不采用通信协议都不会影响到我们的通信,只不过采用通信协议加以限制后,我们可以对我们的通信内容或者通信流程加以限制,使我们的通信更合规合理合法
TCP通信协议 TCP协议可以将我们的整个通信流程分为两个大的阶段 也就是三次握手建立连接与四次挥手断开连接 """ 三次握手建立连接 """ 三次握手建立连接也就是在通信建立之前 先由客户端向服务端发出建立通信的请求 然后服务端在收到请求后反馈给客户端 建立客户端到服务端的单向通信通道 然后由服务端向客户端发出建立通信的请求 当客户端得到请求后向服务端反馈 建立服务端到客户端的单向通信 这样就完成了双向通道的建立 由于服务端向客户端反馈结果的同时 也可以完成坚立通信请求的传递 所以这两步可以合并为一步 也就是我们提到的三次握手的由来 正是因为有了这个反馈机制 TCP协议才会被称为可靠协议 建立通信后,当从客户端向服务端发送数据时 客户端会在本地保留一个数据副本 只有得到服务端的反馈后才会删除该副本 否则服务端会在一定时间内反复尝试发送 而当多个用户端同时向服务端发送请求时 就会导致洪水攻击的出现 导致服务端崩溃,一直处于SYN_RCVD状态 而服务端实现区分客户端请求的方式 就是对服务端发来的请求做一个唯一的标识 """ 四次挥手断开连接 """ 四次挥手断开连接与三次握手的大致流程一致 只不过是由客户端向服务端发出断开请求 得到服务端反馈后断开客户端到服务端的单向通信 然后由服务端向客户端确认是否数据全部发送 在得到客户端反馈后 断开服务端到客户端的单向通信 正是因为有等待确认的机制 所以四次挥手不可以合并为三次
三次握手建立连接
四次挥手断开连接
UDP协议 UDP协议全称是用户数据报协议在 网络中它与TCP协议一样用于处理数据 是一种无连接的协议 UDP有不提供数据包分组、组装 和不能对数据包进行排序的缺点 也就是说,当报文发送之后 是无法得知其是否安全完整到达的。 它有以下几个特点: 1. 面向无连接 首先UDP是不需要和TCP一样 在发送数据前进行三次握手建立接的 想发数据就可以开始发送了 并且也只是数据报文的搬运工 不会对数据报文进行任何拆分和拼接操作 具体来说就是: 在发送端应用层将数据传递给传输层的UDP协议 UDP只会给数据增加一个UDP头标识下是UDP协议 然后就传递给网络层了 在接收端网络层将数据传递给传输层 UDP只去除IP报文头就传递给应用层 不会任何拼接操作 2. 有单播,多播,广播的功能 UDP不止支持一对一的传输方式 同样支持一对多,多对多,多对一的方式 也就是说UDP提供了单播,多播,广播的功能。 3. UDP是面向报文的 发送方的UDP对应用程序交下来的报文 在添加首部后就向下交付IP层 UDP对应用层交下来的报文 既不合并,也不拆分,而是保留这些报文的边界 因此,应用程序必须选择合适大小的报文 4. 不可靠性 首先不可靠性体现在无连接上 通信都不需要建立连接,想发就发 这样的情况肯定不可靠 并且收到什么数据就传递什么数据 并且也不会备份数据 发送数据也不会关心对方是否已经正确接收到数据了 再者网络环境时好时坏但是UDP因为没有拥塞控制 一直会以恒定的速度发送数据 即使网络条件不好,也不会对发送速率进行调整 这样实现的弊端就是在网络条件不好的情况下 可能会导致丢包但是优点也很明显 在某些实时性要求高的场景使用UDP而不是TCP。
UDP协议
应用层
应用层相当于是程序员自己写的应用程序 里面的协议非常的多
常见的有:HTTP、HTTPS、FTP等等
socket模块
如果我们需要编写基于网络进行数据交互的程序 意味着我们需要自己通过代码来控制我们之前所学习的OSI七层(很繁琐 很复杂 类似于我们自己编写操作系统)
socket类似于操作系统,封装了丑陋复杂的接口提供简单快捷的接口socket也叫套接字
基于文件类型的套接字家族(单机)
AF_UNIX
基于网络类型的套接字家族(联网)
AF_INETsocket代码简介
""" 服务端代码 """ import socket # 1.产生一个socket对象并指定采用的通信版本和协议(TCP) server = socket.socket() # 括号内不写参数 默认就是TCP协议 family=AF_INET基于网络的套接字 type=SOCK_STREAM流式协议即TCP # 2.绑定一个固定的地址(服务端必备的条件) server.bind(('127.0.0.1', 8080)) # 127.0.0.1为本地回环地址 只有自己的电脑可以访问 # 3.设立半连接池(暂且忽略) server.listen(5) # 4.等待接客 sock, addr = server.accept() # return sock, addr 三次握手 print(sock, addr) # sock就是双向通道 addr就是客户端地址 # 5.服务客人 data = sock.recv(1024) # 接收客户端发送过来的消息 1024字节 print(data.decode('utf8')) sock.send('尊敬的客人 您说什么就是什么 一切按照您的要求来'.encode('utf8')) # 给客户端发送消息 注意消息必须是bytes类型 # 6.关闭双向通道 sock.close() # 四次挥手 # 7.关闭服务端 server.close() # 店倒闭了
""" 客户端代码 """ import socket # 1.生成socket对象指定类型和协议 client = socket.socket() # 2.通过服务端的地址链接服务端 client.connect(('127.0.0.1', 8080)) # 3.直接给服务端发送消息 client.send('大爷有钱 把你们店最好的给我叫出来'.encode('utf8')) # 4.接收服务端发送过来的消息 data = client.recv(1024) print(data.decode('utf8')) # 5.断开与服务端的链接 client.close()
借助以上的代码,我们就简单实现了C/S架构
代码优化
1.聊天内容自定义
针对消息采用input获取
2.让聊天循环起来
将聊天的部分用循环包起来
3.用户输入的消息不能为空
本质其实是两边不能都是recv或者send 一定是一方收一方发
4.服务端多次重启可能会报错
Address already in use 主要是mac电脑会报
方式1:改端口号
方式2:博客里面代码拷贝即可
5.当客户端异常断开的情况下 如何让服务端继续服务其他客人
windows服务端会直接报错
mac服务端会有一段时间反复接收空消息延迟报错
异常处理、空消息判断""" 优化后的服务端代码 """ import socket from socket import SOL_SOCKET, SO_REUSEADDR server = socket.socket() server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) server.bind(('127.0.0.1', 8081)) server.listen(5) while True: # 链接循环 sock, addr = server.accept() while True: # 通信循环 try: data = sock.recv(1024) if len(data) == 0: break print(f'来自于客户端{addr}的消息>>>:', data.decode('utf8')) msg = input('请输入发送给客户端的消息(不能发空消息)>>>:').strip() sock.send(msg.encode('utf8')) except BaseException: break
""" 优化后的客户端代码 """ import socket client = socket.socket() client.connect(('127.0.0.1', 8081)) while True: msg = input('请输入您想要发送给服务端的消息>>>:').strip() if len(msg) == 0: print('不能发送空消息') continue client.send(msg.encode('utf8')) data = client.recv(1024) print('来自于服务端发送过来的消息>>>:', data.decode('utf8'))
半连接池(缓冲池)
server.listen(5) # 半连接池 当有多个客户端来链接的情况下 我们可以设置等待数量(不考虑并发问题) 假设服务端只有一个人的情况下 在测试半连接池的时候 可以不用input获取消息 直接把消息写死即可
黏包现象
黏包现象的产生:
1.服务端连续执行三次recv
2.客户端连续执行三次send
问题:服务端一次性接收到了客户端三次的消息,该现象称为"黏包现象"
黏包现象产生的原因:
1.不知道每次的数据到底多大
2.TCP也称为流式协议:数据像水流一样绵绵不绝没有间隔(TCP会针对数据量较小且发送间隔较短的多条数据一次性合并打包发送)根据黏包现象的产生,我们可以知道,想要避免黏包现象的产生,可以在接收数据前提前获知数据的具体大小,来定义接收数据时的参数
struct模块
import struct info = b'hello big baby' print(len(info)) # 数据真实的长度(bytes) 14 res = struct.pack('i', len(info)) # 将数据打包成固定的长度 i是固定的打包模式 print(len(res)) # 打包之后长度为(bytes)4,这种格式我们称之为报头 real_len = struct.unpack('i', res) print(real_len) # (14,) 根据固定长度的报头,解析出真实数据的长度 desc = b'hello my baby I will take you to play big ball' print(len(desc)) # 数据真实的长度(bytes) 46 res1 = struct.pack('i', len(desc)) print(len(res1)) # 打包之后长度为(bytes)4 real_len1 = struct.unpack('i', res1) print(real_len1) # (46,)根据固定长度的报头,解析出真实数据的长度 """ 解决黏包问题最初方案 客户端 1.将真实数据转成bytes类型并计算长度 2.利用struct模块将真实长度制作一个固定长度的报头 3.将固定长度的报头先发送给服务端 服务端只需要在recv括号内填写固定长度的报头数字即可 4.然后再发送真实数据 服务端 1.服务端先接收固定长度的报头 2.利用struct模块反向解析出真实数据长度 3.recv接收真实数据长度 """ 问题1:struct模块无法打包数据量较大的数据,切换其他打包模式也无法解决问题 res = struct.pack('i', 12313213123) print(res) 问题2:报头能否传递更多的信息,在传输数据大小时,传递更多数据的相关信息 ''' 最终解决方案:字典作为报头打包,效果更好,数字更小 ''' data_dict = { 'file_name': 'xxx老师教学.avi', 'file_size': 123132131232342342423423423423432423432, 'file_info': '内容很精彩 千万不要错过', 'file_desc': '一代神作 私人珍藏' } import json data_json = json.dumps(data_dict) print(len(data_json.encode('utf8'))) # 真实字典的长度 228 res = struct.pack('i', len(data_json.encode('utf8'))) print(len(res)) """ 黏包问题最终方案 客户端 1.制作真实数据的信息字典(数据长度、数据简介、数据名称) 2.利用struct模块制作字典的报头 3.发送固定长度的报头(解析出来是字典的长度) 4.发送字典数据 5.发送真实数据 服务端 1.接收固定长度的字典报头 2.解析出字典的长度并接收 3.通过字典获取到真实数据的各项信息 4.接收真实数据长度 """
黏包现象解决代码示例
""" 服务端代码 """ import socket import struct import json server = socket.socket() server.bind(('127.0.0.1', 8081)) server.listen(5) sock, addr = server.accept() # 1.接收固定长度的字典报头 data_dict_head = sock.recv(4) # 2.根据报头解析出字典数据的长度 data_dict_len = struct.unpack('i', data_dict_head)[0] # 3.接收字典数据 data_dict_bytes = sock.recv(data_dict_len) data_dict = json.loads(data_dict_bytes) # 自动解码再反序列化 # 4.获取真实数据的各项信息 total_size = data_dict.get('file_size') with open(data_dict.get('file_name'), 'wb') as f: f.write(sock.recv(total_size)) ''' 接收真实数据的时候,如果数据量非常大,recv括号内直接填写该数据量不符合实际,可以尝试一点一点接收 ''' total_size = data_dict.get('file_size') recv_size = 0 with open(data_dict.get('file_name'), 'wb') as f: while recv_size < total_size: data = sock.recv(1024) f.write(data) recv_size += len(data) print(recv_size) """ 客户端代码 """ import socket import os import struct import json client = socket.socket() client.connect(('127.0.0.1', 8081)) '''任何文件都是下列思路...''' # 1.获取真实数据大小 file_size = os.path.getsize(r'/Users/jiboyuan/PycharmProjects/day36/xx老师合集.txt') # 2.制作真实数据的字典数据 data_dict = { 'file_name': '有你好看.txt', 'file_size': file_size, 'file_desc': '内容很长 准备好吃喝 我觉得营养快线挺好喝', 'file_info': '这是我的私人珍藏' } # 3.制作字典报头 data_dict_bytes = json.dumps(data_dict).encode('utf8') data_dict_len = struct.pack('i', len(data_dict_bytes)) # 4.发送字典报头 client.send(data_dict_len) # 报头本身也是bytes类型 我们在看的时候用len长度是4 # 5.发送字典 client.send(data_dict_bytes) # 6.最后发送真实数据 with open(r'/Users/jiboyuan/PycharmProjects/day36/xx老师合集.txt', 'rb') as f: for line in f: # 因为TCP流式协议的特性,一行行发送和直接一起发效果一样 client.send(line) import time time.sleep(10)
UDP协议[]
1.UDP服务端和客户端'各自玩各自的' 2.UDP不会出现多个消息发送合并
并发编程理论
并发编程理论:研究网络编程其实就是在研究计算机的底层原理及发展史
"""
计算机中真正干活的是CPU
"""
操作系统发展史1.穿孔卡片阶段
计算机很庞大,使用很麻烦 一次只能给一个人使用,期间很多时候计算机都不工作
好处:程序员独占计算机,为所欲为 坏处:计算机利用率太低 浪费资源
穿孔卡片
穿孔卡片制作
使用穿孔卡片的初代计算机
2.联机批处理系统
提前使用磁带一次性录入多个程序员编写的程序,然后交给计算机执行
CPU工作效率有所提升,不用反复等待程序录入
3.脱机批处理系统
极大地提升了CPU的利用率
总结:CPU提升利用率的过程多道技术
""" 在学习并发编程的过程中,不做刻意提醒的情况下,默认一台计算机就一个CPU """ 单道技术 所有的程序排队执行 过程中不能重合 多道技术 利用空闲时间提前准备其他数据 最大化提升CPU利用率 多道技术详细 1.切换 计算机的CPU在两种情况下会切换(调用CPU运行其他程序) 1.程序有IO操作 输入\输出操作 input、time.sleep、read、write 2.程序长时间占用CPU 让多个程序都能被CPU运行一下 2.保存状态 CPU每次切换走之前都需要保存当前操作的状态,下次切换回来基于上次的进度继续执行
就像服务员同时服务多桌客人一样,只要其中一桌客人有停顿,服务员就立马去其他桌服务别的客人,永远不会停下
进程理论
进程与程序的区别 程序:一堆死代码(还没有被运行起来) 进程:正在运行的程序(被运行起来了) 进程的调度算法(重要) 1.FCFS(先来先服务) 对短作业不友好 2.短作业优先调度 对长作业不友好 3.时间片轮转法+多级反馈队列(目前还在用) 将时间均分 然后根据进程时间长短再分多个等级 等级越靠下表示耗时越长 每次分到的时间越多 但是优先级越低
进程的并行与并发
并行 多个进程同时执行,必须要有多个CPU参与,单个CPU无法实现并行 并发 多个进程看上去像同时执行,单个CPU可以实现,多个CPU也可以 判断下列两句话孰对孰错 我写的程序很牛逼,运行起来之后可以实现14个亿的并行量(X) 并行量必须要有对等的CPU才可以实现 我写的程序很牛逼,运行起来之后可以实现14个亿的并发量(√) 合情合理,完全可以实现,以后我们的项目一般都会追求高并发
进程的三状态
就绪态 所有的进程在被CPU执行之前都必须先进入就绪态等待 运行态 CPU正在执行 阻塞态 进程运行过程中出现了IO操作 阻塞态无法直接进入运行态 需要先进入就绪态
同步与异步
用来表达任务的提交方式
同步
提交完任务之后原地等待任务的返回结果,期间不做任何事异步
提交完任务之后不愿地等待任务的返回结果,直接去做其他事,有结果自动通知阻塞与非阻塞
用来表达任务的执行状态
阻塞
阻塞态
非阻塞
就绪态、运行态综合使用
1.同步阻塞:
客户端发送请求给服务揣,此时服务端处理任务时间很久,则客户端则被服务端堵塞了,所以客户端会一直等待服务端的响应,此时客户端不能做其他任何事,服务端也不接受其他客户揣的请求。这种通信机制比较简单租暴,但是效率不高。
举例:
一个进程运行,执行内部函数的时候进程要等待返回结果,这个时候cpu发现你现在在等待,cpu就不给你用了,让你进入阻塞状态。然后你因为在等待返回结果,没有继续运行别的函数,所以你处于同步状态。2.同步非阻塞:
客户端发送请求给服务端,此时服务端处理任务时间很久,这个时候虽然客户端会一直等待响应,但是服务端可以处理其他的请求,过一会回来处理原先的。这种方式很高效,一个服务端可以处理很多请求,不会在因为任务没有处理完而堵着,所以这是非阻塞的。
举例:
一个进程运行,执行内部函数的时候进程要等待返回结果,这个时候cpu发现你现在在等待,但是他没有直接不给你用,在给你用的同时他也处理别的进程的任务,这个时候就是非阻塞态。但是进程虽然用着cpu,可是因为没收到返回值代码一直停着没继续往下,所以他还是在同步态。3.异步阻塞:
客户端发送请求给服务端,此时服务端处理任务时间很久,但是客户端不会等待服务器响应,它可以做其他的任务,等服务器处理完毕后再把结果响应给客户端,客户端得到回调后再处理服务端的响应。这种方式可以避免客户端一直处于等待的状态,优化了用户体验,其实就是类似于网页里发起的ajax异步请求。
举例:
一个进程运行,执行内部函数的时候进程要等待返回结果,这个时候cpu发现你现在在等待,cpu就不给你用了,让你进入阻塞状态。然后这个进程不会一直等待最终的结果,他会继续运行别的函数,当结果出来的时候,通知一下这个进程就好了。因为进程停下来等待结果,所以处于异步状态。4.异步非阻塞:
客户端发送请求给服务端,此时服务端处理任务时间很久,这个时候的任务虽然处理时间会很久,但是客户端可以做其他的任务,因为他是异步的,可以在回调函数里处理响应;同时服务端是非阻塞的,所以服务端可以去处理其他的任务,如此,这个模式就显得非常的高效了 。
举例:
一个进程运行,执行内部函数的时候进程要等待返回结果,这个时候cpu发现你现在在等待,但是他没有直接不给你用,在给你用的同时他也处理别的进程的任务,这个时候就是非阻塞态。然后这个进程不会一直等待最终的结果,他会继续运行别的函数,当结果出来的时候,通知一下这个进程就好了。因为进程停下来等待结果,所以处于异步状态。创建进程的多种方式
""" 1.鼠标双击软件图标 2.python代码创建进程 """ from multiprocessing import Process import time def task(name): print('task is running',name) time.sleep(3) print('task is over',name) """ 在不同的操作系统中创建进程底层原理不一样 windows 以导入模块的形式创建进程 linux/mac 以拷贝代码的形式创建进程 """ if __name__ == '__main__': p1 = Process(target=task, args=('jason',)) # 位置参数 p1 = Process(target=task, kwargs={'name':'jason123'}) # 关键字参数 p1.start() # 异步 告诉操作系统创建一个新的进程 并在该进程中执行task函数 # task() # 同步 print('主') from multiprocessing import Process import time class MyProcess(Process): def __init__(self, name, age): super().__init__() self.name = name self.age = age def run(self): print('run is running', self.name, self.age) time.sleep(3) print('run is over', self.name, self.age) if __name__ == '__main__': obj = MyProcess('jason', 123) obj.start() print('主')
进程间数据隔离
同一台计算机上的多个进程数据是严格意义上的物理隔离(默认情况下) from multiprocessing import Process import time money = 1000 def task(): global money money = 666 print('子进程的task函数查看money', money) if __name__ == '__main__': p1 = Process(target=task) p1.start() # 创建子进程 time.sleep(3) # 主进程代码等待3秒 print(money) # 主进程代码打印money
进程的join方法
from multiprocessing import Process import time def task(name, n): print('%s is running' % name) time.sleep(n) print('%s is over' % name) if __name__ == '__main__': p1 = Process(target=task, args=('jason1', 1)) p2 = Process(target=task, args=('jason2', 2)) p3 = Process(target=task, args=('jason3', 3)) p.start() # 异步 '''主进程代码等待子进程代码运行结束再执行''' p.join() print('主') start_time = time.time() p1.start() p1.join() p2.start() p2.join() p3.start() p3.join() p1.join() p2.join() p3.join() print(time.time() - start_time) # 3秒多
IPC机制
IPC:进程间通信 消息队列:存储数据的地方 所有人都可以存 也都可以取 from multiprocessing import Queue q = Queue(3) # 括号内可以指定存储数据的个数 # 往消息队列中存放数据 q.put(111) # print(q.full()) # 判断队列是否已满 q.put(222) q.put(333) # print(q.full()) # 判断队列是否已满 # 从消息队列中取出数据 print(q.get()) print(q.get()) # print(q.empty()) # 判断队列是否为空 print(q.get()) # print(q.empty()) # 判断队列是否为空 # print(q.get()) print(q.get_nowait()) """ full() empty() 在多进程中都不能使用!!! """ from multiprocessing import Process, Queue def product(q): q.put('子进程p添加的数据') def consumer(q): print('子进程获取队列中的数据', q.get()) if __name__ == '__main__': q = Queue() # 主进程往队列中添加数据 # q.put('我是主进程添加的数据') p1 = Process(target=consumer, args=(q,)) p2 = Process(target=product, args=(q,)) p1.start() p2.start() print('主')
生产者消费者模型
"""回想爬虫""" 生产者 负责产生数据的'人' 消费者 负责处理数据的'人' 该模型除了有生产者和消费者之外还必须有消息队列(只要是能够提供数据保存服务和提取服务的理论上都可以)
进程对象的多种方法
1.如何查看进程号 from multiprocessing import Process, current_process current_process() current_process().pid import os os.getpid() os.getppid() 2.终止进程 p1.terminate() ps:计算机操作系统都有对应的命令可以直接杀死进程 3.判断进程是否存活 p1.is_alive() 4.start() 5.join()
守护进程
守护进程会随着守护的进程结束而立刻结束 from multiprocessing import Process import time def task(name): print('德邦总管:%s' % name) time.sleep(3) print('德邦总管:%s' % name) if __name__ == '__main__': p1 = Process(target=task, args=('大张红',)) p1.daemon = True p1.start() time.sleep(1) print('恕瑞玛皇帝嗝屁了')
僵尸进程与孤儿进程
僵尸进程 进程执行完毕后并不会立刻销毁所有的数据 会有一些信息短暂保留下来 比如进程号、进程执行时间、进程消耗功率等给父进程查看 ps:所有的进程都会变成僵尸进程 孤儿进程 子进程正常运行 父进程意外死亡 操作系统针对孤儿进程会派遣福利院管理
多进程数据错乱问题
模拟抢票软件 from multiprocessing import Process import time import json import random # 查票 def search(name): with open(r'data.json', 'r', encoding='utf8') as f: data = json.load(f) print('%s在查票 当前余票为:%s' % (name, data.get('ticket_num'))) # 买票 def buy(name): # 再次确认票 with open(r'data.json', 'r', encoding='utf8') as f: data = json.load(f) # 模拟网络延迟 time.sleep(random.randint(1, 3)) # 判断是否有票 有就买 if data.get('ticket_num') > 0: data['ticket_num'] -= 1 with open(r'data.json', 'w', encoding='utf8') as f: json.dump(data, f) print('%s买票成功' % name) else: print('%s很倒霉 没有抢到票' % name) def run(name): search(name) buy(name) if __name__ == '__main__': for i in range(10): p = Process(target=run, args=('用户%s'%i, )) p.start() """ 多进程操作数据很可能会造成数据错乱>>>:互斥锁 互斥锁 将并发变成串行,牺牲了效率但是保障了数据的安全 """
多进程实现TCP服务端并发
import socket from multiprocessing import Process def get_server(): server = socket.socket() server.bind(('127.0.0.1', 8080)) server.listen(5) return server def get_talk(sock): while True: data = sock.recv(1024) print(data.decode('utf8')) sock.send(data.upper()) if __name__ == '__main__': server = get_server() while True: sock, addr = server.accept() p = Process(target=get_talk, args=(sock,)) p.start()
互斥锁代码实操
互斥锁的概念
互斥锁: 对共享数据进行锁定,保证同一时刻只能有一个线程去操作。注意:
互斥锁是多个线程一起去抢,抢到锁的线程先执行,没有抢到锁的线程需要等待,等互斥锁使用完释放后,其它等待的线程再去抢这个锁。
互斥锁的使用
threading模块中定义了Lock变量,这个变量本质上是一个函数,通过调用这个函数可以获取一把互斥锁。from multiprocessing import Process, Lock import time import json import random def search(name): with open(r'data.json', 'r', encoding='utf8') as f: data = json.load(f) print('%s查看票 目前剩余:%s' % (name, data.get('ticket_num'))) def buy(name): # 先查询票数 with open(r'data.json', 'r', encoding='utf8') as f: data = json.load(f) # 模拟网络延迟 time.sleep(random.randint(1, 3)) # 买票 if data.get('ticket_num') > 0: with open(r'data.json', 'w', encoding='utf8') as f: data['ticket_num'] -= 1 json.dump(data, f) print('%s 买票成功' % name) else: print('%s 买票失败 非常可怜 没车回去了!!!' % name) def run(name, mutex): search(name) mutex.acquire() # 抢锁 buy(name) mutex.release() # 释放锁 if __name__ == '__main__': mutex = Lock() # 产生一把锁 for i in range(10): p = Process(target=run, args=('用户%s号' % i, mutex)) p.start()
ps: 建议只加载操作数据的部分
否则整个程序的效率会极低
锁有很多种 但是作用都一样acquire和release方法之间的代码 同一时刻只能有一个线程去操作 如果在调用acquire方法的时候 其他线程已经使用了这个互斥锁 那么此时acquire方法会堵塞 直到这个互斥锁释放后才能再次上锁
死锁现象
这里讲的就是前一个线程拿了a的锁然后想去拿b的锁,同时后一个
进程拿了b的锁,想去拿a的锁,双方都拿不到,代码就卡在那里不会动了,这个现象叫做死锁现象acquire() release() from threading import Thread, Lock import time mutexA = Lock() # 产生一把锁 mutexB = Lock() # 产生一把锁 class MyThread(Thread): def run(self): self.func1() self.func2() def func1(self): mutexA.acquire() print(f'{self.name}抢到了A锁') mutexB.acquire() print(f'{self.name}抢到了B锁') mutexB.release() print(f'{self.name}释放了B锁') mutexA.release() print(f'{self.name}释放了A锁') def func2(self): mutexB.acquire() print(f'{self.name}抢到了B锁') time.sleep(1) mutexA.acquire() print(f'{self.name}抢到了A锁') mutexA.release() print(f'{self.name}释放了A锁') mutexB.release() print(f'{self.name}释放了B锁') for i in range(10): obj = MyThread() obj.start() """ 互斥锁的作用 保证同一时刻只能有一个线程去操作共享数据 保证共享数据不会出现错误问题 使用互斥锁的好处确保某段关键代码只能由一个线程从头到尾完整地去执行 使用互斥锁会影响代码的执行效率 多任务改成了单任务执行 互斥锁如果没有使用好容易出现死锁的情况 """
线程理论
进程
进程其实是资源单位,表示一块内存空间,就像运行py文件的时候一个py文件就是一个进程
线程
线程才是执行单位,表示真正的代码指令,相当于一个py文件内部的代码,每个进程内部最少有一个线程,另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源
一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行由于线程之间的相互制约,致使线程在运行中呈现出间断性,线程也有就绪、阻塞和运行三种基本状态。
就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待CPU
运行状态是指线程占有CPU正在运行;
阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行
每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身线程是程序中一个单一的顺序控制流程。进程内有一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指令运行时的程序的调度单位
在单个程序中同时运行多个线程完成不同的工作,称为多线程
线程是程序中一个单一的顺序控制流程。进程内有一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指令运行时的程序的调度单位,在单个程序中同时运行多个线程完成不同的工作,称为多线程
我们可以将进程比喻是车间
线程是车间里面的流水线为什么要使用多线程?
1.线程在程序中是独立的、并发的执行流。与分隔的进程相比,进程中线程之间的隔离程度要小,它们共享内存、文件句柄和其他进程应有的状态
2.因为线程的划分尺度小于进程,使得多线程程序的并发性高。进程在执行过程之中拥有独立的内存单元,而多个线程共享内存,从而极大的提升了程序的运行效率
3.
线程比进程具有更高的性能,这是由于同一个进程中的线程都有共性,多个线程共享一个进程的虚拟空间。线程的共享环境包括进程代码段、进程的共有数据等,利用这些共享的数据,线程之间很容易实现通信。
4.
操作系统在创建进程时,必须为改进程分配独立的内存空间,并分配大量的相关资源,但创建线程则简单得多。因此,使用多线程来实现并发比使用多进程的性能高得要多多进程的优点:
进程之间不能共享内存,但线程之间共享内存非常容易
操作系统在创建进程时,需要为该进程重新分配系统资源,但创建线程的代价则小得多。因此,使用多线程来实现多任务并发执行比使用多进程的效率高
Python
语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了Python的多线程编程
线程与进程的区别
1)地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
2)通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
3)调度和切换:线程上下文切换比进程上下文切换要快得多。
4)在多线程操作系统中,进程不是一个可执行的实体。线程的特点
1.
一个进程内可以开设多个线程
同一个进程下的多个线程数据是共享的
创建进程与线程的区别:创建进程的消耗要远远大于线程
四、创建线程的两种方式
跟进程一样,也是函数和面向对象两种方式创建线程需要用到Threading.Thread类,他和Process模块使用的方法很像 from threading import Thread from multiprocessing import Process import time def task(name): print(f'{name} is running') time.sleep(0.1) print(f'{name} is over') if __name__ == '__main__': start_time = time.time() p_list = [] for i in range(100): p = Process(target=task, args=('用户%s' % i,)) p.start() p_list.append(p) for p in p_list: p.join() print(time.time() - start_time) t_list = [] for i in range(100): t = Thread(target=task, args=('用户%s' % i,)) t.start() t_list.append(t) for t in t_list: t.join() print(time.time() - start_time) t = Thread(target=task, args=('jason',)) t.start() print('主线程') """ 创建线程无需考虑反复执行的问题 """ class MyThread(Thread): def run(self): print('run is running') time.sleep(1) print('run is over') obj = MyThread() obj.start() print('主线程')
五、线程的诸多特性
join方法(让线程从异步变成同步)from threading import Thread import time def task(name): print(f'{name} is running') time.sleep(1) print(f'{name} is over') t = Thread(target=task, args=('jason',)) t.start() t.join() print('主线程') """ 同进程内多个线程数据共享(可以看成一个py文件内的变量是通用的) """
current_thread().name查看线程的名称
active_count()查看主线程的名称
GIL全局解释器锁
全局解释器锁GIL
GIL:又称全局解释器锁。作用就是限制多线程同时执行,保证同一时间内只有一个线程在执行。线程非独立的,所以同一进程里线程是数据共享,当各个线程访问数据资源时会出现“竞争”状态,即数据可能会同时被多个线程占用,造成数据混乱,这就是线程的不安全。所以引进了互斥锁,确保某段关键代码、共享数据只能由一个线程从头到尾完整地执行
为什么会有GIL
Python为了利用多核CPU,开始支持多线程。而解决多线程之间数据完整性和状态同步的最简单方法自然就是加锁,于是有了GIL这把超级大锁。因为有了GIL,所以我们的Python可以实现多进程,但是这是一个假的多进程,虽然它会利用多个CPU共同协作,但实则是利用一个CPU的资源
但是这种GIL导致我们的多进程并不是真正的多进程,所以它的效率很低。但当大家试图去拆分和去除GIL的时候,发现大量库代码开发者已经重度依赖GIL而非常难以去除了。如果推到重来,多线程的问题依然还是要面对,但是至少会比目前GIL这种方式会更优雅。所以简单的说:GIL的存在更多的是历史原因
GIL的副作用
Python的多线程在多核CPU上,只对于IO密集型计算产生正面效果;而当有至少有一个CPU密集型线程存在,那么多线程效率会由于GIL而大幅下降。正因为有了GIL的存在,我们Python的多线程效率才会比较低,毕竟它不是真正的多线程。那么此时,我们就可以考虑使用多进程去实现,因为多进程是可以利用多核的CPU资源的。但是又有一个问题?多进程需要的资源较大,明显不是最好的解决办法,那么如何高效的解决这一问题呢?
我们都知道Python它其实是一个“胶水”语言,它除了可以调用自己的模块。类库之外,还可以调用C、C + +等语言的很多模块、类库。此时,我们只需加载动态库,把多进程这块,换成利用C语言去实现就可以了
GIL的总结
因为GIL的存在,只有IO Bound场景下的多线程会得到较好的性能。
如果对并行计算性能较高的程序可以考虑把核心部分也换成C模块,或者索性用其他语言实现
GIL在较长一段时间内将会继续存在,但是会不断对其进行改进。官方文档对GIL的解释:
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes
at once.This lock is necessary mainly because CPython’s memory
management is not thread - safe.(However, since the GIL exists, other
features have grown to depend on the guarantees that it enforces.在CPython解释器中存在全局解释器锁简称GIL
python解释器有很多类型
CPython
JPython
PyPython(常用的是CPython解释器)GIL本质也是一把互斥锁
用来阻止同一个进程内多个线程同时执行(重要)GIL的存在是因为CPython解释器中内存管理不是线程安全的(垃圾回收机制)
当我们在运行python解释器的时候,之所以不能弄成多线程,是因为处于多线程状态下的时候,垃圾回收机制会误删数据值,在数据值还没有和变量名绑定的时候,就会提前把数据值删除,造成异常,因此python中使用的是GIL
来控制线程的运行先后顺序验证GIL的存在
根据GIL的作用我们得知,线程都是串行运行的,因此这里我们调用time模块让每个线程运行时睡0.1秒,来验证代码运行一百次是否需要10秒以上。
import time from threading import Thread, Lock num = 100 def task(mutex): global num mutex.acquire() count = num time.sleep(0.1) num = count - 1 mutex.release() mutex = Lock() t_list = [] for i in range(100): t = Thread(target=task, args=(mutex,)) t.start() t_list.append(t) for t in t_list: t.join() print(num)
GIL与普通互斥锁
既然CPython解释器中有GIL
那么我们以后写代码是不是就不需要操作锁了!!!回答:GIL只能够确保同进程内多线程数据不会被垃圾回收机制弄乱。并不能确保程序里面的数据是否安全。
import time from threading import Thread, Lock num = 100 def task(mutex): global num mutex.acquire() count = num time.sleep(0.1) num = count - 1 mutex.release() mutex = Lock() t_list = [] for i in range(100): t = Thread(target=task, args=(mutex,)) t.start() t_list.append(t) for t in t_list: t.join() print(num)
python多线程是否有用
需要分情况
情况1
单个CPU
多个CPU
情况2
IO密集型(代码有IO操作)
计算密集型(代码没有IO)
单个CP
UIO密集型
多进程申请额外的空间消耗更多的资源
多线程消耗资源相对较少,通过多道技术计算密集型
多进程申请额外的空间消耗更多的资源(总耗时 + 申请空间 + 拷贝代码 + 切换)
多线程消耗资源相对较少通过多道技术(总耗时 + 切换)多个CPU
IO密集型
多进程
总耗时(单个进程的耗时 + IO + 申请空间 + 拷贝代码)
多线程
总耗时(单个进程的耗时 + IO)计算密集型
多进程
总耗时(单个进程的耗时)
多线程
总耗时(多个进程的综合)from threading import Thread from multiprocessing import Process import os import time def work(): # 计算密集型 res = 1 for i in range(1, 100000): res *= i if __name__ == '__main__': # print(os.cpu_count()) # 12 查看当前计算机CPU个数 start_time = time.time() # p_list = [] # for i in range(12): # 一次性创建12个进程 # p = Process(target=work) # p.start() # p_list.append(p) # for p in p_list: # 确保所有的进程全部运行完毕 # p.join() t_list = [] for i in range(12): t = Thread(target=work) t.start() t_list.append(t) for t in t_list: t.join() print('总耗时:%s' % (time.time() - start_time)) # 获取总的耗时 """ 计算密集型 多进程:5.665567398071289 多线程:30.233906745910645 """
def work(): time.sleep(2) # 模拟纯IO操作 if __name__ == '__main__': start_time = time.time() # t_list = [] # for i in range(100): # t = Thread(target=work) # t.start() # t_list.append(t) # for t in t_list: # t.join() p_list = [] for i in range(100): p = Process(target=work) p.start() p_list.append(p) for p in p_list: p.join() print('总耗时:%s' % (time.time() - start_time)) """ IO密集型 多线程:0.0149583816528320 多进程:0.6402878761291504 """
信号量
在python并发编程中信号量相当于多把互斥锁(设置多少信号量,同一时间最多可以运行多少把互斥锁)
概念
信号量(英语:semaphore)又称为信号标,
是一个同步对象,用于保持在0至指定最大值之间的一个计数值。
当线程完成一次对该semaphore对象的等待(wait)时,该计数值减一;
当线程完成一次对semaphore对象的释放(release)时,计数值加一
当计数值为0,则线程等待该semaphore对象不再能成功直至该semaphore对象变成signaled状态。
semaphore对象的计数值大于0,为signaled状态;计数值等于0,为nonsignaled状态.from threading import Thread, Lock, Semaphore import time import random sp = Semaphore(5) # 一次性产生五把锁 class MyThread(Thread): def run(self): sp.acquire() print(self.name) time.sleep(random.randint(1, 3)) sp.release() for i in range(20): t = MyThread() t.start()
event事件
在初始情况下, Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假, 那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个Event对象的信号标志设置为真, 它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象, 那么它将忽略这个事件, 继续执行。
from threading import Thread, Event import time event = Event() # 类似于造了一个红绿灯 def light(): print('红灯亮着的 所有人都不能动') time.sleep(3) print('绿灯亮了 油门踩到底 给我冲!!!') event.set() def car(name): print('%s正在等红灯' % name) event.wait() print('%s加油门 飙车了' % name) t = Thread(target=light) t.start() for i in range(20): t = Thread(target=car, args=('熊猫PRO%s' % i,)) t.start()
进程池与线程池
概念介绍
在程序实际处理问题过程中,忙时会有成千上万的任务需要被执行,闲时可能只有零星任务。那么在成千上万个任务需要被执行的时候,我们就需要去创建成千上万个进程么?首先,创建进程需要消耗时间,销毁进程也需要消耗时间。第二即便开启了成千上万的进程,操作系统也不能让他们同时执行,这样反而会影响程序的效率。因此我们不能无限制的根据任务开启或者结束进程。那么我们要怎么做呢?在这里,要给大家介绍一个进程池的概念,定义一个池子,在里面放上固定数量的进程,有需求来了,就拿一个池中的进程来处理任务,等到处理完毕,进程并不关闭,而是将进程再放回进程池中继续等待任务。如果有很多任务需要执行,池中的进程数量不够,任务就要等待之前的进程执行任务完毕归来,拿到空闲进程才能继续执行。也就是说,池中进程的数量是固定的,那么同一时间最多有固定数量的进程在运行。这样不会增加操作系统的调度难度,还节省了开闭进程的时间,也一定程度上能够实现并发效果。
进程和线程能否无限制的创建?
不可以
因为硬件的发展赶不上软件,有物理极限。如果我们在编写代码的过程中无限制的创建进程或者线程可能会导致计算机崩溃。作用
池
降低程序的执行效率
但是保证了计算机硬件的安全
进程池
提前创建好固定数量的进程供后续程序的调用
超出则等待
线程池
提前创建好固定数量的线程供后续程序的调用
超出则等待concurrent.fututres
模块
ProcessPoolExecutor
类–进程池开启
进程池类的导入:from concurrent.fututres import ProcessPoolExecutor实例化:pool_p = ProcessPoolExecutor(整数): 实例化获得一个进程池, 参数传入一个整数,代表进程池的大小
不传的话会默认开设当前计算机CPU个数的进程
方法及属性介绍
异步提交任务–submit方法
pool_p.submit(task, n=i): task–提交的任务,逗号之后可以按照位置参数或者关键字参数传入task所需的参数;submit方法 :会有一个返回值,返回一个Future对象: < Future
at 内存地址 state = ......returned..... >Future对象会有一个result方法
from concurrent.futures import ProcessPoolExecutor import time pool1 = ProcessPoolExecutor(5) def task(n): print(n) time.sleep(2) if __name__ == '__main__': for i in range(5): pool1.submit(task, i) print('main process')
由结果不难发现submit方法是异步提交任务
from concurrent.futures import ProcessPoolExecutor import time pool1 = ProcessPoolExecutor(5) def task(n): print(n, end=" ") time.sleep(1) if __name__ == '__main__': for i in range(20): pool1.submit(task, i) print('main process')
result方法submit方法返回的Future对象会有一个result方法
result方法会返回 提交的任务最终返回的结果
from concurrent.futures import ProcessPoolExecutor import time pool1 = ProcessPoolExecutor(5) def task(n): print(n, end=' ') time.sleep(1) if __name__ == '__main__': for i in range(5): res = pool1.submit(task, i) print(res.result()) print('main process')
修改提交任务的返回值–验证
from concurrent.futures import ProcessPoolExecutor import time pool1 = ProcessPoolExecutor(5) def task(n): print(n, end=' ') time.sleep(1) return n ** 2 if __name__ == '__main__': for i in range(5): res = pool1.submit(task, i) print(res.result()) print('main process')
shutdown方法
关闭线程池,等待线程池中所有的任务全部运行结束if __name__ == '__main__': l = [] for i in range(10): res = pool1.submit(task, i) l.append(res) pool1.shutdown() for res in l: print('返回值:', res.result()) print('main process')
将所有的任务全部提交运行结束之后统一获得结果
做法参考创建线程的第一种方式add_done_callback–异步回调机制
from concurrent.futures import ProcessPoolExecutor import time pool1 = ProcessPoolExecutor(5) def task(n): print('--调用函数:', n) time.sleep(0.5) return n ** 2 def call_back(feature): print('!!获得函数结果:', feature.result()) if __name__ == '__main__': for i in range(10): res = pool1.submit(task, i) time.sleep(0.2) res.add_done_callback(call_back)
注意:对于add_done_callback(callback)
传入的函数名callback, 在定义callback函数的时候一定要写一个位置参数这个位置参数会通过add_done_callback(callback)
方法自动传入, 而且这个参数就是
调用add_done_callback方法的feature对象ThreadPoolExecutor类–线程池的开启
方法与属性
与进程池完全一致from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor import os import time import random from threading import current_thread 产生含有固定数量线程的线程池 pool = ThreadPoolExecutor(10) pool = ProcessPoolExecutor(5) def task(n): print('task is running') #time.sleep(random.randint(1, 3)) #print('task is over', n, current_thread().name) #print('task is over', os.getpid()) return '我是task函数的返回值' def func(*args, **kwargs): print('from func') if __name__ == '__main__': # 将任务提交给线程池即可 for i in range(20): #res = pool.submit(task, 123) #朝线程池提交任务 #print(res.result()) #不能直接获取 #pool.submit(task, 123).add_done_callback(func)
协程
"""
进程:资源单位
线程:执行单位
协程:单线程下实现并发(效率极高)
在代码层面欺骗CPU,让CPU觉得我们的代码里面没有IO操作
实际上IO操作被我们自己写的代码检测 一旦有 立刻让代码执行别的
(该技术完全是程序员自己弄出来的 名字也是程序员自己起的)
核心:自己写代码完成切换+保存状态
"""import time from gevent import monkey; monkey.patch_all() # 固定编写 用于检测所有的IO操作(猴子补丁) from gevent import spawn def func1(): print('func1 running') time.sleep(3) print('func1 over') def func2(): print('func2 running') time.sleep(5) print('func2 over') if __name__ == '__main__': start_time = time.time() #func1() #func2() s1 = spawn(func1) # 检测代码 一旦有IO自动切换(执行没有io的操作 变向的等待io结束) s2 = spawn(func2) s1.join() s2.join() print(time.time() - start_time) # 8.01237154006958 协程 5.015487432479858
协程实现并发
同时运行两个线程,然后用代码检测I / O操作,然后让两者在I / O操作的时候不中断非阻塞态(不适用协程的时候,应该是串行方式运行代码,也就是同步的方式)
import socket from gevent import monkey; monkey.patch_all() # 固定编写 用于检测所有的IO操作(猴子补丁) from gevent import spawn def communication(sock): while True: data = sock.recv(1024) print(data.decode('utf8')) sock.send(data.upper()) def get_server(): server = socket.socket() server.bind(('127.0.0.1', 8080)) server.listen(5) while True: sock, addr = server.accept() # IO操作 spawn(communication, sock) s1 = spawn(get_server) s1.join()
如何不断的提升程序的运行效率
多进程下开多线程
多线程下开协程