python-网络编程

Day 29

网络基础知识

架构

  • C/S架构: client客户端和server服务端
    优势:能充分发挥PC机的性能。

  • B/S架构:browser浏览器和server服务器,隶属于C/S架构
    优势:无需下载APP与升级客户端,减少客户端压力。

通信

  1. 同一台电脑的两个py程序通信:打开一个文件,读写
    image

  2. 两个电脑通信:连接一根网线
    image

  3. 多个电脑通信:
    image
    a. 源主机server1发送一个请求帧(我的ip是192.168.1.1,我的mac地址是xxxxxx,我请求的ip地址是192.168.1.2)给交换机;
    b. 交换机广播这条消息给所有主机;
    c. 目标主机server2收到消息后发现自己就是被请求的机器,回复交换机信息(我的ip是192.168.1.2,我的mac地址是yyyyyy,回复的主机ip是192.168.1.1,mac地址是xxxxxx)
    d.交换机单播形式返回给源主机

广播

  主机之间“一对所有”的通讯模式,网络对其中每一台主机发出的信号都进行无条件复制并转发,所有主机都可以接收到所有信息(不管你是否需要),由于其不用路径选择,所以其网络成本可以很低廉。有线电视网就是典型的广播型网络,我们的电视机实际上是接受到所有频道的信号,但只将一个频道的信号还原成画面。在数据网络中也允许广播的存在,但其被限制在二层交换机的局域网范围内,禁止广播数据穿过路由器,防止广播数据影响大面积的主机。

ip地址与ip协议

规定网络地址的协议叫ip协议,它定义的地址称之为ip地址,广泛采用的v4版本即ipv4,它规定网络地址由32位2进制表示
范围0.0.0.0-255.255.255.255
一个ip地址通常写成四段十进制数,例:172.16.10.1
端口
  我们知道,一台拥有IP地址的主机可以提供许多服务,比如Web服务、FTP服务、SMTP服务等,这些服务完全可以通过1个IP地址来实现。那么问题来了,主机是怎样区分不同的网络服务呢?举个栗子,For Example,当你和你的朋友聊QQ的时候,你把一条信息发送给对方,为什么那条消息能显示到对方电脑上的QQ对话框,而不是显示到微信的对话框??显然不只有一个ip地址是不行的,因为IP 地址与网络服务的关系是一对多的关系。实际上是通过“IP地址+端口号”来区分不同的服务的。ip找到对应主机,端口找到主机中某一个服务。

mac地址

  head中包含的源和目标地址由来:ethernet规定接入internet的设备都必须具备网卡,发送端和接收端的地址便是指网卡的地址,即mac地址。

  mac地址:每块网卡出厂时都被烧制上一个世界唯一的mac地址,长度为48位2进制,通常由12位16进制数表示(前六位是厂商编号,后六位是流水线号)

arp协议 ——查询IP地址和MAC地址的对应关系

  地址解析协议,即ARP(Address Resolution Protocol),是根据目标IP地址获取目标物理地址的一个TCP/IP协议。
  主机发送信息时将包含目标IP地址的ARP请求广播到网络上的所有主机,并接收返回消息,以此确定目标的物理地址。
  收到返回消息后将该IP地址和物理地址存入本机ARP缓存中并保留一定时间,下次请求时直接查询ARP缓存以节约资源。
  地址解析协议是建立在网络中各个主机互相信任的基础上的,网络上的主机可以自主发送ARP应答消息,其他主机收到应答报文时不会检测该报文的真实性就会将其记入本机ARP缓存;由此攻击者就可以向某一主机发送伪ARP应答报文,使其发送的信息无法到达预期的主机或到达错误的主机,这就构成了一个ARP欺骗。ARP命令可用于查询本机ARP缓存中IP地址和MAC地址的对应关系、添加或删除静态对应关系等。相关协议有RARP、代理ARP。NDP用于在IPv6中代替地址解析协议。

广域网与路由器

image

路由器

  路由器(Router),是连接因特网中各局域网、广域网的设备,它会根据信道的情况自动选择和设定路由,以最佳路径,按前后顺序发送信号。 路由器是互联网络的枢纽,"交通警察"。目前路由器已经广泛应用于各行各业,各种不同档次的产品已成为实现各种骨干网内部连接、骨干网间互联和骨干网与互联网互联互通业务的主力军。路由和交换机之间的主要区别就是交换机发生在OSI参考模型第二层(数据链路层),而路由发生在第三层,即网络层。这一区别决定了路由和交换机在移动信息的过程中需使用不同的控制信息,所以说两者实现各自功能的方式是不同的。
  路由器(Router)又称网关设备(Gateway)是用于连接多个逻辑上分开的网络,所谓逻辑网络是代表一个单独的网络或者一个子网。当数据从一个子网传输到另一个子网时,可通过路由器的路由功能来完成。因此,路由器具有判断网络地址和选择IP路径的功能,它能在多网络互联环境中,建立灵活的连接,可用完全不同的数据分组和介质访问方法连接各种子网,路由器只接受源站或其他路由器的信息,属网络层的一种互联设备。

路由器与交换机的区别
交换机的主要功能是组织局域网,经过交换机内部处理解析信息之后,将信息以点对点,点多对的形式,发送给固定端。
路由器的主要功能是跨网段的数据传输,路由选择最佳路径。
例如:
  如果你需要将多台电脑连接到一根网线, 用交换机即可
  如果你只有一个外网ip,但是你有好多台电脑需要上网, 用路由器即可

局域网

  局域网(Local Area Network,LAN)是指在某一区域内由多台计算机互联成的计算机组。一般是方圆几千米以内。局域网可以实现文件管理、应用软件共享、打印机共享、工作组内的日程安排、电子邮件和传真通信服务等功能。局域网是封闭型的,可以由办公室内的两台计算机组成,也可以由一个公司内的上千台计算机组成。  

子网掩码

  所谓”子网掩码”,就是表示子网络特征的一个参数。它在形式上等同于IP地址,也是一个32位二进制数字,它的网络部分全部为1,主机部分全部为0。比如,IP地址172.16.10.1,如果已知网络部分是前24位,主机部分是后8位,那么子网络掩码就是11111111.11111111.11111111.00000000,写成十进制就是255.255.255.0。

  知道”子网掩码”,我们就能判断,任意两个IP地址是否处在同一个子网络。方法是将两个IP地址与子网掩码分别进行AND运算(两个数位都为1,运算结果为1,否则为0),然后比较结果是否相同,如果是的话,就表明它们在同一个子网络中,否则就不是。

比如,已知IP地址172.16.10.1和172.16.10.2的子网掩码都是255.255.255.0,请问它们是否在同一个子网络?两者与子网掩码分别进行AND运算,

172.16.10.1:10101100.00010000.00001010.000000001
255255.255.255.0:11111111.11111111.11111111.00000000
AND运算得网络地址结果:10101100.00010000.00001010.000000001->172.16.10.0



172.16.10.2:10101100.00010000.00001010.000000010
255255.255.255.0:11111111.11111111.11111111.00000000
AND运算得网络地址结果:10101100.00010000.00001010.000000001->172.16.10.0
结果都是172.16.10.0,因此它们在同一个子网络。

总结一下,IP协议的作用主要有两个,一个是为每一台计算机分配IP地址,另一个是确定哪些地址在同一个子网络。

互联网协议与osi模型

osi模型

image

每层常见的设备与协议

osi模型 设备 协议
应用层 http、https、ftp
传输层 四层路由器、四层交换机 tcp、udp
网络层 路由器、三层交换机 ip
数据链路层 以太网路交换机、网卡、网桥 arp
物理层 集线器、网线、光纤 电信号、光信号

tcp与udp协议

TCP协议(传输控制协议)提供的是面向连接、可靠的字节流服务。当客户和服务器彼此交换数据前,必须先在双方之间建立一个TCP连接,之后才能传输数据。TCP提供超时重发,丢弃重复数据,检验数据,流量控制等功能,保证数据能从一端传到另一端。

tcp的连接

image
ACK : 确认收到
SYN : 请求连接的这么一个标识
FIN : 请求断开的这么一个标识

TCP是因特网中的传输层协议,使用三次握手协议建立连接。当主动方发出SYN连接请求后,等待对方回答SYN+ACK[1],并最终对对方的 SYN 执行 ACK 确认。这种建立连接的方法可以防止产生错误的连接。[1]
TCP三次握手的过程如下:
客户端发送SYN(SEQ=x)报文给服务器端,进入SYN_SEND状态。
服务器端收到SYN报文,回应一个SYN (SEQ=y)ACK(ACK=x+1)报文,进入SYN_RECV状态。
客户端收到服务器端的SYN报文,回应一个ACK(ACK=y+1)报文,进入Established状态。
三次握手完成,TCP客户端和服务器端成功地建立连接,可以开始传输数据了。
建立一个连接需要三次握手,而终止一个连接要经过四次握手,这是由TCP的半关闭(half-close)造成的。
(1) 某个应用进程首先调用close,称该端执行“主动关闭”(active close)。该端的TCP于是发送一个FIN分节,表示数据发送完毕。
(2) 接收到这个FIN的对端执行 “被动关闭”(passive close),这个FIN由TCP确认。
注意:FIN的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程,放在已排队等候该应用进程接收的任何其他数据之后,因为,FIN的接收意味着接收端应用进程在相应连接上再无额外数据可接收。
(3) 一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。
(4) 接收这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN。[1]
既然每个方向都需要一个FIN和一个ACK,因此通常需要4个分节。
注意:
(1) “通常”是指,某些情况下,步骤1的FIN随数据一起发送,另外,步骤2和步骤3发送的分节都出自执行被动关闭那一端,有可能被合并成一个分节。[2]
(2) 在步骤2与步骤3之间,从执行被动关闭一端到执行主动关闭一端流动数据是可能的,这称为“半关闭”(half-close)。
(3) 当一个Unix进程无论自愿地(调用exit或从main函数返回)还是非自愿地(收到一个终止本进程的信号)终止时,所有打开的描述符都被关闭,这也导致仍然打开的任何TCP连接上也发出一个FIN。
无论是客户还是服务器,任何一端都可以执行主动关闭。通常情况是,客户执行主动关闭,但是某些协议,例如,HTTP/1.0却由服务器执行主动关闭。[2]

UDP协议
UDP协议(用户数据报协议)在网络中它与TCP协议一样用于处理数据包,是一个简单的面向数据报的运输层协议。UDP不提供可靠性,它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地。由于UDP在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故而传输速度很快。

tcp和udp的对比
UDP TCP
是否连接 无连接 面向连接
是否可靠 不可靠传输,不使用流量控制和拥塞控制 可靠传输,使用流量控制和拥塞控制
连接对象个数 支持一对一,一对多,多对一和多对多交互通信 只能是一对一通信
传输方式 面向报文 面向字节流
首部开销 首部开销小,仅8字节 首部最小20字节,最大60字节
适用场景 适用于实时应用(IP电话、视频会议、直播等) 适用于要求可靠传输的应用,例如文件传输

tcp为什么比udp更可靠?
tcp是面向连接的,udp是无连接的;
tcp通信过程中有ack,是确认收到消息的标识。

socket模块

socket是一个模块,是一个套接字,是一个类,是传输层和应用层之间的一个抽象层。

socket概念

image
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

分类:

  • 基于文件类型的套接字家族
    套接字家族的名字:AF_UNIX

unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信

  • 基于网络类型的套接字家族
    套接字家族的名字:AF_INET

(还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,在网络编程中,大部分时候使用AF_INET)

socket模块

image

TCP协议对应socket的方法

obj=socket.socket(family = AF_INET,type = SOCK_STREAM)  #创建套接字,默认为网络类型套接字,tcp协议(SOCK_STREAM表示tcp,SOCK_DGRAM表示udp)
obj.bind(("ip",port)) #为服务端绑定ip端口
obj.listen() #打开服务端口,并监听外部请求
obj.connect(("ip",port)) #客户端连接服务端口  在此处与服务端发生三次握手
obj.connect_ex不会因为对方已关闭连接而报错。(("ip",port))#与connect不同的是connect_ex不会因为对方已关闭连接而报错。
conn,addr=obj.accept() #接收客户端连接,conn为新的套接字对象,addr为对方的(ip,port)  在此处与客户端发生三次握手
obj.send(smsg.encode("utf-8")) #发送编码后的信息给对方
rmsg=conn.recv(num).decode("utf-8")  最多接收到num个bytes类型的对方发送的数据,需要解码
conn.close() #关闭连接 在此处发生4次挥手
obj.close()  #关闭套接字

创建一个简单的tcp数据传输服务

server:

import socket
obj=socket.socket(type=socket.SOCK_STREAM)
obj.bind(("127.0.0.1",18080))
obj.listen()
connt,addr=obj.accept()
rmsg=connt.recv(10).decode("utf-8")
print(rmsg)
connt.close()
obj.close()

client:
import socket
obj=socket.socket(type=socket.SOCK_STREAM)
obj.connect(("127.0.0.1",18080))
obj.send("你好".encode("utf-8"))
obj.close()

server可以与其他多个人a,b,c聊天的程序,tcp只能做到与a聊天结束后,再与b聊天。

server:
---------------------------------------------
import socket
obj=socket.socket(type=socket.SOCK_STREAM)
obj.bind(("127.0.0.1",18080))
obj.listen()

while 1:
    connt,addr=obj.accept()
    while 1:
        rmsg=connt.recv(10).decode("utf-8")
        print(rmsg)
        if rmsg == "q":
            break
        smsg=input("<<<")
        connt.send(smsg.encode("utf-8"))
        if smsg == "q":
            break
    connt.close()

obj.close()

client:
--------------------------------------------
import socket
obj=socket.socket(type=socket.SOCK_STREAM)
obj.connect(("127.0.0.1",18080))
while 1:
    smsg=input("<<<")
    obj.send(smsg.encode("utf-8"))
    if smsg == "q":
        break
    rmsg=obj.recv(1024).decode("utf-8")
    print(rmsg)
    if rmsg == "q":
        break
obj.close()

使用tcp进行小文件的上传下载

server:
import socket
import json
import os

obj=socket.socket()
obj.bind(("127.0.0.1",18888))
obj.listen()
conn,addr=obj.accept()
msg=conn.recv(9090).decode("utf-8")
msg=json.loads(msg)

if msg["func"]=="upload":
    with open("new_"+msg["filename"],"w") as f:
        f.write(msg["content"])

if msg["func"]=="download":
    print([msg])
    print(msg["filename"])
    download_file=os.path.join("D:\Desktop\python_script\download",msg["filename"])
    with open(download_file,encoding="utf-8") as f:
        content=f.read().encode("utf-8")
        conn.send(content)

conn.close()
obj.close()
----------------------------------------------------------------------
client:

import socket
import json
import os

func={"1":"download","2":"upload"}
dic={"func":None,"filename":None,"content":None}

for key,value in func.items():
    print(key,value)
active=input("请输入需要操作的序号:")

obj=socket.socket()
obj.connect_ex(("127.0.0.1",18888))

if active=="1":
    filename=input("请输入要下载的文件名:")
    dic["func"]="download"
    dic["filename"]=filename
    dic_download=json.dumps(dic)
    obj.send(dic_download.encode("utf-8"))
    file_content=obj.recv(9090).decode("utf-8")
    print(file_content)
    with open("new_"+filename,"w",encoding="utf-8") as f:
        f.write(file_content)

if active=="2":
    path_file=input("请输入要上传的文件:")
    filename=os.path.basename(path_file)
    with open(path_file,"r",encoding="utf-8") as f:
        content=f.read()
    dic["func"]="upload"
    dic["filename"]=filename
    dic["content"]=content
    dic_upload=json.dumps(dic)
    obj.send(dic_upload.encode("utf-8"))

obj.close()

UDP协议对应socket的方法

obj=socket.socket(family = AF_INET,type = SOCK_DGRAM)  #创建套接字,默认为网络类型套接字、tcp协议(SOCK_STREAM表示tcp,SOCK_DGRAM表示udp)
obj.bind(("ip",port)) #为服务端绑定ip端口
obj.sendto(smsg.encode("utf-8"),("ip",port)) 发送信息给ip:port
rmsg,addr=obj.recvfrom(num) 最多接收到num个bytes类型的对方发送的数据,rmsg为字节码类型,addr为对方的(ip,port)
obj.close() #关闭套接字

创建一个简单的udp数据传输服务

server:
-----------------------------------------
import socket
obj=socket.socket(type=socket.SOCK_DGRAM)
obj.bind(("127.0.0.1",18888))
rmsg,addr=obj.recvfrom(1024)
print(rmsg.decode("utf-8"))
obj.close()

client:
-----------------------------------------
import socket
obj=socket.socket(type=socket.SOCK_DGRAM)
obj.sendto("hello".encode("utf-8"),("127.0.0.1",18888))
obj.close()

server同时与多人聊天,带姓名,服务端,不同人名显示不同颜色

server:
-----------------------------------------------------
import socket

contact_list={"王耍耍":"\033[32m","李搓搓":"\033[35m"}

sc=socket.socket(type=socket.SOCK_DGRAM)
sc.bind(("127.0.0.1",8800))
name=input("input urname:")
while 1:
    getmsg,addr=sc.recvfrom(1024)
    getmsg=getmsg.decode("utf-8")
    color=contact_list.get(getmsg.split(":")[0].strip(),"")
    print("%s%s\033[0m" %(color,getmsg))
    if getmsg=="q":
        print("886")
        continue
    sendmsg=input("<<<")
    sc.sendto((name+": "+sendmsg).encode("utf-8"),addr)
    if sendmsg=="q":
        print("886")
        break
sc.close

client:
----------------------------------------------------
import socket

sc=socket.socket(type=socket.SOCK_DGRAM)
name=input("input urname:")
while 1:
    sendmsg=input("<<<")
    sc.sendto((name+": "+sendmsg).encode("utf-8"),("127.0.0.1",8800))
    if sendmsg=="q":
        print("886")
        break
    getmsg,addr=sc.recvfrom(1024)
    getmsg=getmsg.decode("utf-8")
    print(getmsg)
    if getmsg=="q":
        print(886)
        break

sc.close

效果:
image

由于每一次收发信息都要编码解码,我们写一个自己的类,来做编解码。

文件 mysocket.py
---------------------------------------------------
import socket

class mysocket(socket.socket):
    def __init__(self,encoding="utf-8"):
        self.encoding=encoding
        return super().__init__(type=socket.SOCK_DGRAM)

    def r(self,num):
        rmsg,addr=super().recvfrom(num)
        rmsg=rmsg.decode(self.encoding)
        return rmsg,addr

    def s(self,msg,addr):
        msg=msg.encode(self.encoding)
        return super().sendto(msg,addr)

文件server
---------------------------------------------------
import mysocket
obj=mysocket.mysocket()
obj.bind(("127.0.0.1",18080))
r_msg,addr=obj.r(1024)
print(r_msg)
obj.close()

文件client
---------------------------------------------------
import mysocket
obj=mysocket.mysocket()
s_msg=input("<<<")
obj.s(s_msg,("127.0.0.1",18080))
obj.close()

tcp的粘包

粘包:当多个数据包合并或者连续发送时,接收方不清楚每条数据的边界,而导致多条信息的全部/部分内容粘连接收为一条的现象。

粘包的原因:

  • 合包机制导致的数据合并
  • 接收端接收数据不及时导致缓存区有多条数据
    例如:
server:
import socket

obj=socket.socket()
obj.bind(("127.0.0.1",18888))
obj.listen()
conn,addr=obj.accept()
msg1=conn.recv(1024).decode("utf-8")
msg2=conn.recv(1024).decode("utf-8")
print("msg1:",msg1)
print("msg2:",msg2)
conn.close()
obj.close()
----------------------------------------------------------
client:
import socket
#import time

obj=socket.socket()
obj.connect_ex(("127.0.0.1",18888))
obj.send(b'hi')
#time.sleep(2)
obj.send(b'nihao')
obj.close()

输出:
msg1: hinihao   #本来发送了两条信息,server接收时合成了一条,发生了粘包。
msg2:           #可以在多条发送信息之间sleep小段时间,避免粘包

拆包机制与合包机制

tcp的发送过程如下:
image
这个图能帮组更好的理解拆包与粘包
image
拆包机制:
两种情况下会发生拆包:

  1. 发送缓存区的剩余空间不够放下整个文件;
  2. 发送超过网卡的MTU的数据包。
    当发送较大数据包时(例如5000bytes),由于受到数据链路层网卡的MTU(Maximum Transmission Unit,大部分网络设备的MTU都是1500bytes)的限制,会将数据包拆成多个小包(1500,1500,1500,500)进行发送,到接收端缓存区时会等待各个小包传输完成合并为一个完整的包。
    image

合包机制:
tcp发送数据时使用了Nagle算法来优化发送速率,即当发送较小的包时,它会有较短时间的等待,如还有其他数据需要发送,就会合成一个包一起发送。
image

udp不粘包

udp协议不会发生粘包现象,因为它不使用nagle算法,有发送需求,就直接发送,由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。接收udp传输的数据必须以消息为单位提取数据,不能一次提取任意字节的数据。即面向消息的通信是有消息保护边界的。

使用udp协议发送数据,一次收发大小究竟多少合适?

udp协议本层对一次收发数据大小的限制是:
    65535 - ip包头(20) - udp包头(8) = 65507
数据链路层,网卡的MTU一般被限制在了1500:
    1500 - ip包头(20) - udp包头(8) = 1472

当传输数据为num个时:
1. num > 65507  报错
2. 1472 < num < 65507  会在数据链路层拆包,而udp本身就是不可靠协议,所以一旦拆包之后,造成的多个小数据包在网络传输中,如果丢任何一个,那么此次数据传输失败
3. num < 1472 是比较理想的状态

总结

总结
黏包现象只发生在tcp协议中:
1.从表面上看,黏包问题主要是因为发送方和接收方的缓存机制、以及tcp协议的合包机制导致。
2.实际上,主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。

如何防止粘包?
传送数据时也传入数据的长度,接收方再根据数据的长度精确获取数据。
例如:
使用tcp传送大文件
版本1:为防止传送的字典与后续的文件粘包,字典传送完后,服务方回复一个消息,传送方进行一次接收。

#server:
# import json
# import socket
# import os

# obj=socket.socket()
# obj.bind(("127.0.0.1",18081))
# obj.listen()
# conn,addr=obj.accept()
# dic=conn.recv(1024)
# dic=json.loads(dic)
# conn.send("ok".encode("utf-8")) #在这里回复一个消息,是防止后续接收粘包

# if dic["opt"] == "上传文件":
#     filename="new"+dic["filename"]
#     filesize=dic["filesize"]
#     with open(filename,"ab") as f:
#         while filesize:
#             ret=conn.recv(1024)
#             f.write(ret)
#             filesize-=len(ret)
#     if os.path.getsize(filename) == dic["filesize"]:
#         conn.send("上传成功".encode("utf-8"))
#     else:
#         conn.send("上传失败".encode("utf-8"))

# if dic["opt"] == "下载文件":
#     pass
# conn.close()
# obj.close()
----------------------------------------------
#client:
# import socket
# import os
# import json

# opts={1:"上传文件",2:"下载文件"}
# for key,value in opts.items():
#     print(key,value)
# get_opt=input("请输入需要使用的功能的序号:")


# if get_opt=="1":
#     get_file=input("请输入需要上传的文件(带上绝对路径):")
#     filename=os.path.basename(get_file)
#     filesize=os.path.getsize(get_file)
#     file_msg={"opt":"上传文件","filename":filename,"filesize":filesize}
#     file_msg=json.dumps(file_msg,ensure_ascii=False)

#     obj=socket.socket()
#     obj.connect(("127.0.0.1",18081))
#     obj.send(file_msg.encode("utf-8"))
#     obj.recv(1024)     #在这里加一个recv是防止file_msg与后续要传输的文件在接收端粘包

#     with open(get_file,"rb") as f:
#         while filesize:
#             content=f.read(1024)
#             obj.send(content)
#             filesize-=len(content)
#     result=obj.recv(1024).decode("utf-8")
#     print(result)

# if get_opt=="2":
#     pass
# obj.close()

版本2:传送字典时带上字典的长度。长度的字符用struct.pack()封装为固定的4个字节,接收方先接收长度的字段,再根据解码后的长度精确接收字典。

#server:
import json
import socket
import os
import struct

obj=socket.socket()
obj.bind(("127.0.0.1",18081))
obj.listen()
conn,addr=obj.accept()
dic_len=struct.unpack("i",conn.recv(4))[0]  #前4个bytes为字典的长度,再根据这个值精确获取到字典,防止粘包。
dic=conn.recv(dic_len)
dic=json.loads(dic)
print(dic)
# conn.send("ok".encode("utf-8"))

if dic["opt"] == "上传文件":
    filename="new"+dic["filename"]
    filesize=dic["filesize"]
    with open(filename,"ab") as f:
        while filesize:
            ret=conn.recv(1024)
            f.write(ret)
            filesize-=len(ret)

    if os.path.getsize(filename) == dic["filesize"]:
        conn.send("上传成功".encode("utf-8"))
    else:
        conn.send("上传失败".encode("utf-8"))

if dic["opt"] == "下载文件":
    pass
conn.close()
obj.close()
----------------------------------------------------------------
#client:
import socket
import os
import json
import struct

opts={1:"上传文件",2:"下载文件"}
for key,value in opts.items():
    print(key,value)
get_opt=input("请输入需要使用的功能的序号:")


if get_opt=="1":
    get_file=input("请输入需要上传的文件(带上绝对路径):")
    filename=os.path.basename(get_file)
    filesize=os.path.getsize(get_file)
    file_msg={"opt":"上传文件","filename":filename,"filesize":filesize}
    file_msg=json.dumps(file_msg,ensure_ascii=False).encode("utf-8")

    obj=socket.socket()
    obj.connect(("127.0.0.1",18081))

    dic_len=struct.pack("i",len(file_msg))  #获取到字典的长度,再将长度自断封装为4bytes的固定长度,server端就能正常获取
    obj.send(dic_len+file_msg)

    with open(get_file,"rb") as f:
        while filesize:
            content=f.read(1024)
            obj.send(content)
            filesize-=len(content)
    result=obj.recv(1024).decode("utf-8")
    print(result)

if get_opt=="2":
    pass
obj.close()

socket方法扩展

send与sendall
socket.send(string[, flags])
send()的返回值是发送的字节数量,这个数量值可能小于要发送的string的字节数,也就是说可能无法发送string中所有的数据,需要分批发送。如果有错误则会抛出异常。
–
socket.sendall(string[, flags])
尝试发送string的所有数据,成功则返回None,失败则抛出异常。

故下面两段代码是等价的:
#sock.sendall('Hello world\n')

#buffer = 'Hello world\n'
#while buffer:
#    bytes = sock.send(buffer) #返回本次发送的长度
#    buffer = buffer[bytes:]
服务端套接字函数
s.bind()    绑定(主机,端口号)到套接字
s.listen()  开始TCP监听
s.accept()  被动接受TCP客户的连接,(阻塞式)等待连接的到来

客户端套接字函数
s.connect()     主动初始化TCP服务器连接
s.connect_ex()  connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

公共用途的套接字函数
s.recv()            接收TCP数据
s.send()            发送TCP数据
s.sendall()         发送TCP数据
s.recvfrom()        接收UDP数据
s.sendto()          发送UDP数据
s.getpeername()     连接到当前套接字的远端的地址 eg: ('127.0.0.1', 18888)
s.getsockname()     当前套接字的地址 eg: ('127.0.0.1', 64542)
s.getsockopt()      返回指定套接字的参数,不知道怎么用
s.setsockopt()      设置指定套接字的参数,不知道怎么用
s.close()           关闭套接字

面向锁的套接字方法
s.setblocking()     设置套接字的阻塞与非阻塞模式,s.setblocking(False)为非阻塞模式,就是在accept,recv的地方不等待
s.settimeout()      设置阻塞套接字操作的超时时间,obj.settimeout(4)设置为4s,超过会timeout报错
s.gettimeout()      得到阻塞套接字操作的超时时间,阻塞模式未设置时间,为一直等待,返回None

面向文件的套接字的函数
s.fileno()          套接字的文件描述符
s.makefile()        创建一个与该套接字相关的文件

socketserver模块

此模块可以解决tcp协议中服务端不能同时连接多个客户端的问题,是处于socket抽象层与应用层之间的一层,更贴近用户。
这里的同时连接客户端是指,支持多个客户端同时连接,但是是处理完一个客户端的请求后,才会再处理第二个客户端的请求。若第一个客户端不退出,第二个客户端也是无法处理的。
使用:

#server:
import socketserver

class Mysocket(socketserver.BaseRequestHandler):
    def handle(self):
            get_str=self.request.recv(1024).decode("utf-8")
            print(get_str)
            get_str=self.request.send(get_str.upper().encode("utf-8"))

server=socketserver.TCPServer(("127.0.0.1",18081),Mysocket)
server.serve_forever()

#client:
import socket
obj=socket.socket()
obj.connect(("127.0.0.1",18081))

st=input("<<<")
obj.send(st.encode("utf-8"))
ret=obj.recv(1024).decode("utf-8")
print(ret)
Day 34

计算机与操作系统简介

发展史

计算机的硬件组成:

主板  固化很多硬件在主板上(寄存器是直接和cpu进行交互的一个有限的高速存储部件,暂存一些指令、数据与地址等)
cpu  中央处理器:计算(数字计算和逻辑计算)和控制(控制所有硬件协调工作)
存储  硬盘、内存
输入设备 键盘、鼠标、话筒
输出设备  屏幕、音响

早期的计算机是以计算为核心的
现在的计算机是以存储为核心的

计算机的发展

第一代计算机:电子管计算机,极其耗电,体积庞大,散热量很高
第二代计算机:晶体管计算机,相较于第一代的耗电、体积、散热量都有优化
第三代计算机:集成电路计算机,一个板子固化几十到上百个小硬件(小时候见到的白色体积很大的那种计算机)。
第四代计算机:大型集成电路计算机,一个板子可以达到固化十万个硬件。
第五代计算机:甚大型集成电路计算机 (目前计算机是否为第五代争议很多)

计算机使用的发展史

  1. 人工时代:穿孔卡带
    程序员写二进制的代码,通过卡带上的穿孔表示01,输入设备读取卡带内容,传入计算机内存进行计算,结果输出为卡带,程序员再读取二进制的结果。
    一个人员在一段时间独占整台计算机。
  2. 脱机时代
    程序员写好程序,将输出输出设备与计算机分离,由操作员将操作计算机,程序员只负责输入与拿到输出结果。
    image
  3. 单道批处理系统
    内存中一次只允许存放一道作业
    image
    在A程序计算时,I/O空闲, A程序I/O操作时,CPU空闲(B程序也是同样);必须A工作完成后,B才能进入内存中开始工作,两者是串行的,全部完成共需时间=T1+T2。
  4. 多道批处理系统
    image
    将A、B两道程序同时存放在内存中,它们在系统的控制下,可相互穿插、交替地在CPU上运行:当A程序因请求I/O操作而放弃CPU时,B程序就可占用CPU运行,这样 CPU不再空闲,而正进行A I/O操作的I/O设备也不空闲,显然,CPU和I/O设备都处于“忙”状态,大大提高了资源的利用率,从而也提高了系统的效率,A、B全部完成所需时间<<T1+T2。
    使CPU得到充分利用,同时改善I/O设备和内存的利用率,从而提高了整个系统的资源利用率和系统吞吐量。
    单处理机系统中多道程序运行时的特点:
      (1)多道:计算机内存中同时存放几道相互独立的程序;
      (2)宏观上并行:同时进入系统的几道程序都处于运行过程中,即它们先后开始了各自的运行,但都未运行完毕;
      (3)微观上串行:实际上,各道程序轮流地用CPU,并交替运行
  5. 分时系统
    分时技术:把处理机的运行时间分成很短的时间片,按时间片轮流把处理机分配给各联机作业使用。
    若某个作业在分配给它的时间片内不能完成其计算,则该作业暂时中断,把处理机让给另一作业使用,等待下一轮时再继续其运行。由于计算机速度很快,作业运行轮转得很快,给每个用户的印象是,好象他独占了一台计算机。

特点:
  (1)多路性。若干个用户同时使用一台计算机。微观上看是各用户轮流使用计算机;宏观上看是各用户并行工作。
  (2)交互性。用户可根据系统对请求的响应结果,进一步向系统提出新的请求。这种能使用户与系统进行人机对话的工作方式,明显地有别于批处理系统,因而,分时系统又被称为交互式系统。
  (3)独立性。用户之间可以相互独立操作,互不干扰。系统保证各用户程序运行的完整性,不会发生相互混淆或破坏现象。
  (4)及时性。系统可对用户的输入及时作出响应。分时系统性能的主要指标之一是响应时间,它是指:从终端发出命令到系统予以应答所需的时间。
6. 实时系统
一般比较少见,主要用于军事和工业生产上。

计算机语言的发展史
机器语言 二进制,由0与1组成;
汇编语言 可以编写操作系统,是机器语言上一层的语言
高级语言 面向过程(C),面向对象:(C++ JAVA PYTHON .NET PHP 等)

操作系统

作用:

  1. 封装所有硬件接口,让各种用户使用电脑更加轻松
  2. 是对计算机内所有资源进行合理的调度和分配
    image

进程

基本概念

什么是进程?
是指正在执行的程序。
是程序执行过程中的一次 指令,数据集等的集合。
也可以叫做程序的一次执行过程。
进程是资源分配的基本单位.

进程由三部分组成:代码段,数据段,PCB(进程管理控制)
进程控制块(PCB)是用来记录进程状态及其他相关信息的数据结构,PCB是进程存在的唯一标志,PCB存在则进程存在。系统创建进程时会产生一个PCB,撤销进程时,PCB也自动消失。

IPC(inter process communication):进程间通信,指两个进程的数据之间产生交互。
正常情况下,多进程之间是无法进行通信的,因为每个进程有自己独立的进程空间。

进程的基本状态:
image

就绪状态:已经获得运行需要的所有资源,除了CPU
执行状态:已经获得了所有资源包括cpu,处于正在运行
阻塞状态:因为各种原因,进程放弃了cpu,导致进程无法继续执行,此时进程处于内存中,继续等待获取cpu
进程的一个特殊状态:
挂起状态:是指因为种原因,进程放弃了cpu,导致进程无法继续执行,此时进程被踢出内存。

并行与并发:

  • 并行
    同一时间点多个任务同时执行,如多条道路同一时间点都有车辆通过,多核cpu同时处理事件就是并行。
  • 并发
    同一时间间隔内(一个范围)多个任务在宏观上同时执行。如一条道路,在某一个时间间隔内有多辆车运行,单核cpu在同一时间间隔内处理多件事情就是并发。(微观上串行,宏观上并行)
    image

同步与异步:

  • 同步
    当在程序a中调用程序b时,a等待b返回结果,再执行后续的代码,就是同步;
    即一个任务a完成需要依赖另一个任务b时,只有被依赖的任务b完成,a才算完成,这是一种可靠的任务序列,要么都成功,要么都失败。
  • 异步
    当在程序a中调用程序b时,a触发b运行后,直接执行后续的代码,就是异步;
    即不需要等待被依赖的任务b完成,只需要告诉告诉它要执行的事情,之后,a也继续执行剩下的程序,a执行完成,就算完,b是否真正完成,a不确定,所以是不可靠的任务序列。

阻塞与非阻塞

  • 阻塞
    就是线程/进程中遇到(IO操作)如磁盘读写或网络通信时,会耗费大量时间,此时操作系统会剥夺这个程序的cpu资源,直到IO操作完成,再分配cpu资源,让其继续执行,例如input("<<<");
  • 非阻塞
    当线程/进程遇到 I/O 操作时,不会以阻塞的方式等待 I/O 操作的完成或数据的返回,而只是将 I/O 请求发送给操作系统,继续执行下一条语句。当操作系统完成 I/O 操作时,以事件的形式通知执行 I/O 操作的线程,线程会在特定时候处理这个事件。

同步与阻塞,异步与非阻塞的区别:
同步与异步是对应的,它们是线程之间的关系,两个线程之间要么是同步的,要么是异步的。
阻塞与非阻塞是对同一个线程来说的,在某个时刻,线程要么处于阻塞,要么处于非阻塞。
阻塞是使用同步机制的结果,非阻塞则是使用异步机制的结果。

创建进程及常用方法与属性

开启子进程的两种方式

#第一种 普通的方法
from multiprocessing import Process
import os

def func(i):
    print(i,"子进程: pid %s ppid %s"%(os.getpid(),os.getppid())) 

if __name__=="__main__":             #这一行必须有,不然会报错
    i=1
    p=Process(target=func,args=(i,))         #定义一个子进程,args为需要传入func的参数,为元组形式。
    p.start()                                  #调用子进程
    print("这是父进程:pid %s" %os.getpid())

#输出:
# 这是父进程:pid 9460       #父进程先执行完自己的代码,等待子进程执行完后,父进程才关闭。
# 1 子进程: pid 920 ppid 9460

###################################
#第二种 类继承
from multiprocessing import Process
import os
class My_process(Process):
    def __init__(self):
        super().__init__()

    def run(self):    #必须定义此方法
        print("子进程",os.getpid())

if __name__=="__main__":
    p=My_process()
    p.start()  # 告诉操作系统,执行这个子进程,但是什么时候执行操作系统说了算,p.start()内部也调用了run() 进入就绪状态
    # p.run()  # 告诉操作系统,现在马上帮我执行这个子进程,强制马上获取cpu资源 直接进入执行状态
    print("父进程",os.getpid())
#输出:
# 父进程 9420
# 子进程 7896

要执行的函数,直接在类中定义

from multiprocessing import Process
class Myprocess(Process):
    def __init__(self,name):
        super().__init__()
        self.name=name
        
    def func(self):
        print("in func,in child,name:%s" % self.name)

    def run(self):
        self.func()

if __name__=="__main__":
    p=Myprocess("杨刷刷")
    p.start()

输出:
in func,in child,name:杨刷刷

正常情况下父进程与子进程之前是异步还是同步呢?

from multiprocessing import Process
import os
import time

def func(i):
    time.sleep(5) 
    print("这是子进程:",i) 

if __name__=="__main__":
    for i in range(2):
        p=Process(target=func,args=(i,))         #定义一个子进程
        p.start()                                 #调用子进程
    time.sleep(1)
    print("这是父进程")
输出:
这是父进程
这是子进程: 1
这是子进程: 0

通过以上输出可以知道,父进程调用子进程后,又继续执行后续的程序了,所以是异步。
可以通过join()方法设置为同步。

from multiprocessing import Process
import os
import time

def func(i):
    time.sleep(5) 
    print("这是子进程:",i) 

if __name__=="__main__":
    for i in range(2):
        p=Process(target=func,args=(i,))         #定义一个子进程
        p.start()                                 #调用子进程
        p.join()      #设置为同步状态
    time.sleep(1)
    print("这是父进程")
输出:
(等待5s)
这是子进程: 0
(等待5s)
这是子进程: 1
(等待1s)
这是父进程

# p.join()# 是让主进程等待子进程执行完。  现象:主进程执行到这句话,主进程阻塞住,等待子进程执行

以上每一次生成子进程前都要等待前面的子进程返回。但很多时候我的需求是生成多个子进程后,再等待所有子进程统一返回,再执行主程序。可以写成:

from multiprocessing import Process
import os
import time

def func(i):
    time.sleep(5) 
    print("这是子进程:",i) 
lst=[]
if __name__=="__main__":
    for i in range(2):
        p=Process(target=func,args=(i,))         #定义一个子进程
        p.start()
        lst.append(p)
    [i.join() for i in lst]
    time.sleep(1)  
    print("这是父进程")
输出:
(等待5s)
这是子进程: 0
这是子进程: 1
(等待1s)
这是父进程

多进程之间无法共享内存,各个进程的执行空间相互隔离,故父进程定义的变量无法直接在子进程中使用,如name1,要使用变量只能在创造子进程时传入,如name2

from multiprocessing import Process
import os

def func(name1):
    print("name1:",name1)
    global name2      # 这两行报错name 'name2' is not defined
    print("name2:",name2)      #
    print("这是子进程") 

if __name__=="__main__":
    name1="aa"
    name2="bb"
    p=Process(target=func,args=(name1,))         #定义一个子进程
    p.start()
    print("这是父进程")

输出:
这是父进程
name1: aa
Process Process-1:
...NameError: name 'name2' is not defined

杀掉子进程与查看状态

from multiprocessing import Process
import time

def func(i):
    print("这是子进程:",i)
    time.sleep(10)

if __name__=="__main__":
    p=Process(target=func,args=(1,))         #定义一个子进程
    p.start()
    p.terminate()         #python解释器告诉操作系统杀掉这个子进程
    print(p.is_alive())   #这时操作系统还没来得及杀掉
    time.sleep(0.02)
    print(p.is_alive())   #这时操作系统已经杀掉子进程
    print("这是父进程")

输出:
True
False
这是父进程

#p.terminate() 杀掉p这个子进程
#p.is_alive() 查看p进程状态,True为活着,False为死了

多进程模块一些常用的其他属性与守护进程

from multiprocessing import Process
from os import name
import time

def func(i):
    print("这是子进程:",i)
    time.sleep(2)

if __name__=="__main__":
    p=Process(target=func,args=(1,))         #定义一个子进程
    p.start()
    p.name="little_chd"  #定义p进程名字
    print("子进程名字是",p.name)       
    print("子进程pid是",p.pid)      #获取p进程pid
    print("子进程是否为守护进程",p.daemon)  #判断p进程是否为守护进程
输出:
子进程名字是 little_chd
子进程pid是 15648
子进程是否为守护进程 False
这是子进程: 1

设置一个进程为守护进程 p.daemon=True
守护进程的特点:

  1. 随着父进程执行结束而结束;
  2. 守护进程不能有子进程。

这里演示守护进程随着父进程内容(不包括等待子进程)执行结束而结束

def func2():
    for i in range(10,13):
        print(i)
        time.sleep(0.1)

def func1():
    for i in range(3):
        print(i)
        time.sleep(0.1)


if __name__=="__main__":
    p1=Process(target=func1,args=())
    p1.start()
    p2=Process(target=func2,args=())
    p2.daemon=True   #设置p2为守护进程
    p2.start()

    print("父进程代码结束,等待子进程返回")
	
输出:
父进程代码结束,等待子进程返回  #这是主进程最后一行代码执行结果,这里执行完守护进程也结束了,所以守护进程内容没输出。
0
1
2

当p2不为守护进程,#p2.daemon=True,输出:
父进程代码结束,等待子进程返回
0
10
11
1
2
12

我们尝试为守护进程p创建一个子进程,得到报错

from multiprocessing import Process
import time

def func2(j):
    time.sleep(1)
    print("这是孙进程:",j)


def func(i):
    p=Process(target=func2,args=(2,)) 
    p.start()
    time.sleep(2)
    print("这是子进程:",i)


if __name__=="__main__":
    p=Process(target=func,args=(1,))
    p.daemon=True
    p.start()
    time.sleep(5)
    print("子进程是否为守护进程",p.daemon)

输出:
...AssertionError: daemonic processes are not allowed to have children
子进程是否为守护进程 True

总结:

进程的两种开启方法

1. p=Process(target=func,(args1,)) 将agrs1为func的传入参数,第一个参数后面一定要有逗号,相当于把func和args1拷贝到新进程的空间。
2. 自定义类,继承Process父类

进程的常用方法与属性

方法:
    p.start()  # 告诉操作系统,执行这个子进程,但是什么时候执行操作系统说了算,p.start()内部也调用了run() 进入就绪状态
    p.run()  # 告诉操作系统,现在马上帮我执行这个子进程,强制马上获取cpu资源 直接进入执行状态
	p.terminate() #强制终止进程p,不会进行任何清理操作,如果p还有子进程就变成了孤儿进程,如果p还保存了一个锁,也不会被释放,进而导致死锁
	p.is_alive() #如果p还在运行就返回True,否则返回False
	p.join(num) #主进程等待p终止(强调主进程处于等待状态,p处于运行状态),num为timeout时间,为空则为一直等待。只有start()开启的进程才能用join(),run()开启的不能用。
	正常开启一个子进程,主进程与子进程为异步关系,可以通过设置p.join()变为同步。

属性:
    p.name #p进程的名称,可自定义 p.name="xxx"
	p.pid #返回p进程的pid
	p.daemon #返回p是否为守护进程,True为是,也可自定义p.daemon=True
	守护进程的两个特点:
        守护进程会随着父进程代码(不包括异步时等待的子进程)的结束而结束
        守护进程不能再创建子进程(不能要孩子)
其他:
    os.getpid()  获取当下进程pid
	os.getppid() 获取当下进程的父进程pid
	开启多进程,必须加上“if __name__=='__main__':”,否则不会执行

共享内存

多进程管理包中有设置共享内存的方法,共享内存设置后,多个进程可共享同一个变量,变量在某一个进程中改变,其他进程访问到的改变量也会变,共享内存很容易造成数据紊乱。

Array函数(数组)

Array函数返回一个shared memory包装类,一般用来定义一个数组
arr=Array('i',range(10)) "i"表示数组中的数据类型是int类型。

from multiprocessing import Process,Array

def test_arr(arr):
    print(list(arr))
    for i in arr:
        arr[i]=i*10

if __name__=="__main__":
    arr=Array('i',range(10))
    p=Process(target=test_arr,args=(arr,))
    p.start()
    p.join()

    print(list(arr))

输出:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 10, 20, 30, 40, 50, 60, 70, 80, 90]

Value函数(数值)

Value函数返回一个shared memory包装类,一般 整数用 i,浮点数用 d 。
格式: var=Value('数据类型',数据本身)
下面模拟一下银行取钱,发现输出结果大多数是100,但偶尔会出现其他数值,是因为改变共享内存的变量时没加锁,导致数据紊乱。

#模拟银行取钱
from multiprocessing import Process,Value,Array
import time
def get_money(num):     #取钱,取50次,每次取1元
    for i in range(50):
        num.value-=1    #获取共享变量的格式为 变量名.value
        # print("get%s:%s"% (i,num.value))

def save_money(num):    #存钱,存50次,每次存1元
    for i in range(50):
        num.value+=1
        # print("save%s:%s"% (i,num.value))

if __name__=="__main__":
    num=Value('i',100)   #原本银行卡中的金额为100,这里定义了共享变量
    #以下取钱与存钱的进程同时运行
    get_m=Process(target=get_money,args=(num,))
    get_m.start()
    save_m=Process(target=save_money,args=(num,))
    save_m.start()
    get_m.join()
    save_m.join()
    
    print("最终银行卡金额为%s" % num.value)

执行5次的输出分别为:
最终银行卡金额为109
最终银行卡金额为100
最终银行卡金额为100
最终银行卡金额为150
最终银行卡金额为100

image

Manager函数(list,dict)

m=multiprocessing.Manager() 生成一个manager实例
num=m.类型(值)

from multiprocessing import Process,Manager

def func(num):
    print("func:",num)
    for key,value in num.items():
        num[key]=str(int(key)+1)
    # for i in range(len(num)):
    #     num[i]-=1

if __name__=="__main__":
    m=Manager()
    num=m.dict({1:"a",2:"b",3:"c"})
    # num=m.list([1,2,3])
    p=Process(target=func,args=(num,))
    p.start()
    p.join()
    print(num)

输出:
func: {1: 'a', 2: 'b', 3: 'c'}
{1: '2', 2: '3', 3: '4'}

锁机制

进程之间数据不共享,但是共享同一套文件系统,所以访问同一个文件,或同一个打印终端,是没有问题的,而共享带来的是竞争,竞争带来的结果就是错乱,如何控制,就是加锁处理。

使用multiprocessing.Lock()生成一个锁,一个锁只有一把钥匙,将需要调用的变量用锁锁起来,在进程a拿到钥匙调用该变量时,进程b想调用这个变量必须等进程a释放钥匙之后解开锁,才能使用。

注: 可以理解为锁了某个变量,但也需要知道,所有子进程是遇到那把锁(l.acquire()),没有钥匙才会阻塞,而不是遇到被锁的变量才阻塞。

l=multiprocessing.Lock()
l.acquire() 上锁
l.release() 解锁

我们这里用锁解决上面银行取钱例子数据紊乱的问题

from multiprocessing import Process,Value,Lock
import time
def get_money(num,l):
    for i in range(50):
        l.acquire()  #对后续程序中用到的变量上锁
        num.value-=1
        print("get_money",num.value)
        l.release()  #解锁,钥匙还回去

def save_money(num,l):
    for i in range(50):
        l.acquire()
        num.value+=1
        print("save_money",num.value)
        l.release()

if __name__=="__main__":
    l=Lock()
    num=Value('i',100)
    get_m=Process(target=get_money,args=(num,l))
    get_m.start()
    save_m=Process(target=save_money,args=(num,l))
    save_m.start()
    get_m.join()
    save_m.join()

    print("最终银行卡金额为%s" % num.value)

以上的l=lock()是互斥锁,一把钥匙只能开一把锁。
还有递归锁l=Rlock(),有一把万能钥匙,可以开多把锁,这一把钥匙同一时间只能一个人拥有,这里不多讲,后续线程的锁机制有详解。

信号机制

sem = multiprocessing.Semaphore(n)
n : 是指初始化一把锁配几把钥匙,一个int型
和multiprocessing.Lock()类似,但是Lock一把锁只有一把钥匙,而这个一把锁可以有多把钥匙,能同时供多个子进程调用

from multiprocessing import Semaphore

l=Semaphore(2) #有两把锁

l.acquire()  #用一把
print(123) 
l.release()  #释放,还剩2把

l.acquire()  #用一把,没释放,还剩一把
print(456)

l.acquire() #再用一把,没释放,还剩0把
print(789)

l.acquire()  #这里又要一把锁,但是已经没锁了,所以程序阻塞在这里,等待其他锁的释放
print(10)

# 输出:
# 123
# 456
# 789

中介带看房

#中介带客户看房
from multiprocessing import Semaphore,Process
import time

def find_house(i,sem):
    sem.acquire()
    time.sleep(3)  #这里是带客户看房的时间为3s
    print("第%s个人看了房"%i)
    sem.release()
if __name__=="__main__":
    sem=Semaphore(3)   #一个锁的三把钥匙代表三个中介。
    #有10个客户,三个中介带了三个客户看房,其他客户必须要等待有中介空出来才能去看
    for i in range(1,11): 
        p=Process(target=find_house,args=(i,sem))
        p.start()
输出:
第1个人看了房
第2个人看了房
第3个人看了房   等待3s
第4个人看了房
第6个人看了房
第8个人看了房   等待3s
第7个人看了房
第5个人看了房
第10个人看了房  等待3s
第9个人看了房

事件机制

event实际上描述的是一种同步的处理事件的方式,可以简单地理解为,不同的进程之间可以利用一些特殊的标识来等待其他进程的某些程序处理完毕。
多个进程都拥有同一个event示例,当某个进程调用 实例.wait()方法时,为阻塞状态。当其他进程的阻塞标识切换为True时,同一时间点 实例.wait()的状态切换为非阻塞。
e=multiprocessing.Event()
e.is_set() #阻塞标识。返回true或False,默认为False
e.wait() #当e.is_set()阻塞标识的值为False,e.wait()这里就会阻塞,反之则非阻塞。
e.set() #将is_set()设为True
e.clear() #将is_set()设为False

红绿灯过车

#模拟红绿灯
from multiprocessing import Process,Event
import time

def traffic_light(e):
    print("\033[31m红灯亮了\033[0m")
    while 1:
        if e.is_set():   #True为非阻塞状态,绿灯
            time.sleep(0.1)  #这里为绿灯时间
            print("\033[31m红灯亮了\033[0m") #绿灯亮完提示红灯亮
            e.clear()
        else:          #False为阻塞状态,红灯
            time.sleep(0.1) #这里为红灯时间
            print("\033[32m绿灯亮了\033[0m")  #红灯亮完提示绿灯亮
            e.set()

def traffic_car(e,i):
    # print(e.is_set())
    e.wait()
    if e.is_set():  
        print("第%s辆车通过"%i)

if __name__=="__main__":
    e=Event()
    light=Process(target=traffic_light,args=(e,))
    light.start()
    for i in range(1,51): 
        car=Process(target=traffic_car,args=(e,i))
        car.start()

输出:
image

无意中发现这么一个问题,输出紊乱。原因猜测:

from multiprocessing import Process,Event
import time

def traffic_light(e):
    while 1:
        if e.is_set():   #True为非阻塞状态,绿灯
         #上面正确的程序是先绿灯sleep,再提示红灯亮,这里是先提示绿灯亮,再绿灯sleep,以及set或clear之后再经过while循环到内部的if判断是需要一定时间的,is_set状态切换到打印出绿灯亮之间有一小段时间间隔,而此时早已通车,所以会有旁边的红灯亮了,下面还有True 通车。
            print("\033[32m绿灯亮了\033[0m",e.is_set()) 
            time.sleep(0.1)  #这里为绿灯时间
            e.clear()
        else:          #False为阻塞状态,红灯
            print("\033[31m红灯亮了\033[0m",e.is_set())
            time.sleep(0.1) #这里为红灯时间
            e.set()

def traffic_car(e,i):
    e.wait()
     # 因为在红绿灯的进程中进行set或clear之后,is_set会立即变为另一种状态,就会立即通车
     # 这里的状态也会立即切换
    # if e.is_set():  
    print("%s 第%s辆车通过"%(e.is_set(),i))

if __name__=="__main__":
    e=Event()
    light=Process(target=traffic_light,args=(e,))
    light.start()
    for i in range(1,51): 
        car=Process(target=traffic_car,args=(e,i))
        car.start()

输出紊乱:
image

Day 37

生产者消费者模型

生产者消费者模型就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。

此模型代码在后续队列中演示。

队列与管道

我们知道为了避免数据紊乱可以加锁,但所有进程操作数据都是串行,效率低。
那有没有同时兼顾高效率还没有数据紊乱的烦恼呢,那就是多进程包提供的基于消息的通信机制:队列与管道

队列(FIFO):first input first output,先进先出。
栈(FILO):first input last output,先进后出的存储顺序。
image

队列Queue

首先要将 import queue 与 from multiprocessing import Queue的两个队列区分开。
queue只用于同一进程中的队列,不能做多进程之间的通信。
Queue 是用于多进程的队列,就是专门用来做进程间通信(IPC)

q = multiprocessing.Queue(num)  #创建一个num长度的队列
num : 队列的最大长度,一个数据为1个长度
q.get()# 阻塞等待获取数据,如果有数据直接获取,如果没有数据,默认阻塞等待
get方法有两个可选参数:blocked和timeout。如果blocked为True(默认值),并且timeout为正值,那么在等待时间内没有取到任何元素,会抛出Queue.Empty异常。如果blocked为False,有两种情况存在,如果Queue有一个值可用,则立即返回该值,否则,如果队列为空,则立即抛出Queue.Empty异常.

q.put(数据a)# 阻塞,如果可以继续往队列中放数据,就直接放,不能放就默认阻塞等待
put方法还有两个可选参数:blocked和timeout。如果blocked为True(默认值),并且timeout为正值,该方法会阻塞timeout指定的时间,直到该队列有剩余的空间。如果超时,会抛出Queue.Full异常。如果blocked为False,但该Queue已满,会立即抛出Queue.Full异常。

q.get_nowait()# 不阻塞,如果有数据直接获取,没有数据就报错。同q.get(blocked=False)
q.put_nowait()# 不阻塞,如果可以继续往队列中放数据,就直接放,不能放就报错。同q.put(blocked=False)

q.empty():调用此方法时q为空则返回True,该结果不可靠,比如在返回True的过程中,如果队列中又加入了项目。
q.full():调用此方法时q已满则返回True,该结果不可靠,比如在返回True的过程中,如果队列中的项目被取走。
q.qsize():返回队列中目前项目的正确数量,结果也不可靠,理由同q.empty()和q.full()一样

生产消费者模型:娃娃生产消费(单个生产者与消费者)

#正常情况下生产者生产数据的时间间隔不固定,可能队列的数据都被消费完,生产者还会往队列中put。那么消费者该怎么判断生产者已经put完成?
# 这一版中解决方法为:
#  在生产者put完后,向队列中put一个结束标识。消费者拿到数据进行判断,如果是结束标识就不再等待,结束消费进程。

from multiprocessing import Process,Queue
import time
import random

def productor(q,version):
    '''生产者,往队列里放数据'''
    for i in range(20):
        info="%s%s号" %(version,i)
        time.sleep(random.random())  #模拟生产数据时间不固定
        q.put(info)
def consumer(q,name):
    '''消费者,拿取队列里的数据'''
    while 1:
        info = q.get()
        if info:
            print("%s拿走了%s"%(name,info))
        else:
            print("娃娃都被拿走了")
            break

if __name__=="__main__":
    q=Queue(10)       #生成一个长度为10的队列
    pro=Process(target=productor,args=(q,"饭仔娃娃"))
    con=Process(target=consumer,args=(q,"yxf"))
    pro.start()
    con.start()

    pro.join()  #生产者子进程结束,主进程加如结束标识。
    q.put(None)

生产消费者模型:娃娃生产消费(多个生产者与消费者)

from multiprocessing import Process,Queue
import time
import random

def productor(q,version):
    '''生产者,往队列里放数据'''
    for i in range(20):
        info="%s%s号" %(version,i)
        time.sleep(random.random())  #模拟生产数据时间不固定
        q.put(info)
def consumer(q,name):
    '''消费者,拿取队列里的数据'''
    while 1:
        info = q.get()
        if info:
            print("%s拿走了%s"%(name,info))
        else:
            print("hi %s娃娃都被拿走了"%name)
            break

if __name__=="__main__":
    q=Queue(10)       #生成一个长度为10的队列

#三个生产者和2个消费者同时生产与消费    
    pro1=Process(target=productor,args=(q,"饭仔娃娃"))
    pro2=Process(target=productor,args=(q,"宅男女神"))
    pro3=Process(target=productor,args=(q,"御姐大波浪"))
    con1=Process(target=consumer,args=(q,"yxf"))
    con2=Process(target=consumer,args=(q,"tgz"))
    lis=[pro1,pro2,pro3,con1,con2]
    [i.start() for i in lis]
    pro1.join()
    pro2.join()
    pro3.join()
    #有两个消费者需要put两个结束标识,每一个消费者拿到一个消费标识就立即结束进程。
    q.put(None)
    q.put(None)

队列JoinableQueue

在上面例子中我们使用了生产者生产完成,向队列put一个结束标识的方法去解决消费者该怎么判断生产者已经生产完成的问题。而JoinableQueue类中提供了解决此问题的方法。

#JoinableQueue([maxsize]):JoinableQueue继承了Queue类,所以Queue的方法它也能调用。同时,JoinableQueue队列允许项目的使用者通知生成者项目已经被成功处理。通知进程是使用共享的信号和条件变量来实现的。

#参数介绍:
    maxsize是队列中允许最大项数,省略则无大小限制。    
  #新增的2个方法介绍:
    q.task_done():使用此方法发出信号,表示q.get()的返回数据已经被处理。一般没消费一次数据便调用一次task_done,当调用此方法的次数等于生产者生产的数据个数,则join将不再阻塞,当次数大于从队列中已消费的数据个数,将引发ValueError异常。
    q.join():生产者调用此方法进行阻塞,直到队列中所有的项目均被处理。阻塞将持续到队列中的每个项目均调用q.task_done()方法为止。

使用举例:

from multiprocessing import Process,JoinableQueue
import time
import random

def productor(q,version):
    '''生产者,往队列里放数据'''
    for i in range(20):
        info="%s%s号" %(version,i)
        time.sleep(random.random())  #模拟生产数据时间不固定
        q.put(info)
    q.join() #会阻塞,直到队列中的所有数据都消费完,当接收的消费次数等于生产的数据个数时,变为非阻塞。
#所以productor会等待consumer消费完数据,才结束。但消费者使用了while 1死循环,消费完数据仍会处于阻塞中,所以将其设置为守护进程,随着主进程的结束而结束。

def consumer(q,name):
    '''消费者,拿取队列里的数据'''
    while 1:
        info = q.get()
        print("%s拿走了%s"%(name,info))
        q.task_done() #每get一个数据就调用一次task_done,join就会接收到消费了一次数据的信号。


if __name__=="__main__":
    q=JoinableQueue(10)       #生成一个长度为10的队列
    pro=Process(target=productor,args=(q,"饭仔"))
    con=Process(target=consumer,args=(q,"yxf"))
    con.daemon=True  #将消费者设置为守护进程
    pro.start()
    con.start()
    pro.join()  #守护进程会随着主进程代码执行结束而结束,而消费者进程结束后,生产者进程才会结束,所以加这一行实际在等待消费者进程的执行。

管道Pipe(了解)

conn1,conn2=multiprocessing.Pipe(duplex=True) #生成一个管道,其中conn1,conn2表示管道两端的连接对象,强调一点:必须在产生Process对象之前产
参数:
duplex:默认管道是全双工的,如果将duplex射成False,conn1只能用于接收,conn2只能用于发送。

conn1.send(数据) 发送一个数据给conn2
conn2.recv() 接收一个conn1发送的数据

conn1.close():关闭连接。如果conn1被垃圾回收,将自动调用此方法

conn1.send_bytes(buffer [, offset [, size]]):通过连接发送字节数据缓冲区,buffer是支持缓冲区接口的任意对象,offset是缓冲区中的字节偏移量,而size是要发送字节数。结果数据以单条消息的形式发出,然后调用c.recv_bytes()函数进行接收
conn2.recv_bytes([maxlength]):接收c.send_bytes()方法发送的一条完整的字节消息。maxlength指定要接收的最大字节数。如果进入的消息,超过了这个最大值,将引发IOError异常,并且在连接上无法进行进一步读取。如果连接的另外一端已经关闭,再也不存在任何数据,将引发EOFError异常。
    

image
单进程内的使用:
一般单进程不用管道,因为变量就可以搞定

from multiprocessing import Pipe
conn1,conn2=Pipe()
conn1.send("aaa")
conn1.send("bbb")
print(conn2.recv())  #一次只能接收一个数据
conn2.send(111)
print(conn2.recv()) 
print(conn1.recv())
输出:
aaa
bbb
111

多进程的管道使用

from multiprocessing import Process,Pipe

def func1(conn):
    conn1,conn2=conn
    conn1.close()  #不用conn1就把他关闭
    try:
        while 1:
            print("接收到",conn2.recv())
            #print("接收到",conn2.recv_bytes())
    except EOFError:   #当conn1关闭,conn2.recv()没有数据接收时就会报错 EOFError
        print("接收完成")
        conn2.close()

if __name__=="__main__":
    conn1,conn2=Pipe()
    p=Process(target=func1,args=((conn1,conn2),)) #这里conn1,conn2都传还是传一个根据你的需求来
    p.start()
    conn2.close()
    for i in range(4):
        conn1.send(i)
        #conn1.send_bytes(str(i).encode("utf-8"))
    conn1.close()  #conn1关闭

输出:
接收到 0
接收到 1
接收到 2
接收到 3
接收完成

进程池

进程池:一个池子,里边有固定数量的进程。这些进程一直处于待命状态,一旦有任务来,马上就让进程去处理。这些进程处理任务处于并行状态,可以节约大量时间。进程池中开启进程的个数最好为(cpu数+1)。
适用于持续的数量较多的任务场景。

如果指定num为3,则进程池会从无到有创建三个进程,然后自始至终使用这三个进程去执行所有任务,不会开启其他进程

p = multiprocessing.Pool(num) 创建num个进程的进程池,最好为os.cpu_count() + 1 个
进程调用的3个方法:
p.map(func,iterable)  父进程与子进程为同步关系,父进程会等待map的子进程执行完成再执行后续的代码
  - func:进程池中的进程执行的任务函数
  - iterable: 可迭代对象,是把可迭代对象中的每个元素依次传给任务函数当参数

  
p.apply(func,args=()) 同步处理,在一个池工作进程中执行func(*args,**kwargs),然后返回结果。
   执行完一个任务,再执行第二关任务,同一时间点只有一个进程,不需要close和join
   进程池中的所有进程是普通进程(主进程需要等待其执行结束)
   
p.apply_async(func,args=(),callback=None) 异步处理,在一个池工作进程中执行func(*args,**kwargs),然后返回结果。
   池中的进程一次性都去执行任务
   
   callback: 回调函数,每当进程池中有进程处理完任务了,返回的结果可以交给回调函数,由回调函数进行进一步的处理,回调函数只有异步才有,同步是没有的
   异步处理任务时,进程池中的所有进程是守护进程(主进程代码执行完毕守护进程就结束)
   异步处理任务时,必须要加上close和join

   回调函数的使用:
     子进程的任务函数的返回值,被当成回调函数的形参接收到,以此进行进一步的处理操作
     回调函数是由主进程调用的,而不是子进程,子进程只负责把结果传递给回调函数

p.close():关闭进程池,防止进一步操作。
p.join():等待所有工作进程退出。此方法只能在close()或terminate()之后调用
ret.get() 用于获取异步apply_async()的返回对象对应的结果。

map的使用

from multiprocessing import Process,Pool
import time
def func(i):
    time.sleep(0.5)
    print(i)
    return (i*10)

if __name__=="__main__":
    po=Pool(4)
    it=range(20)          #共执行20次func,使用进程池中的4个进程,一个进程每次执行一个func
    ret=po.map(func,it)   #使用map,父进程在这里会等待子进程执行完成,才继续执行接下来的代码
    print(ret)
    print("主进程代码执行结束")
输出:
(暂停0.5s) 0    
4
6
2 (暂停0.5s)
1
5
3
7 (暂停0.5s)
...
[0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190]
主进程代码执行结束 

apply的使用

from multiprocessing import Pool
import time

def func(i):
    time.sleep(0.5)
    print(i)
    return (i*10)

if __name__=="__main__":
    po=Pool(4)
    lis=[]
    for i in range(3):
        ret=po.apply(func,args=((i,))) #同步,使用进程池中的进程,一次执行一个任务,完成后再执行下一个
        lis.append(ret)
    print(lis)

输出:
0  #每隔0.5s出来一个
1
2
[0, 10, 20]

apply_async的使用
获取网页状态码

# 获取多个网页的状态码

from multiprocessing import Pool
import os
import time
import requests

def func1(url,):
    print(url)
    res=requests.get(url)
    time.sleep(1) #为了更明显的看出并行效果
    return (url,res.status_code)

if __name__=="__main__":
    url=["http://www.baidu.com","https://www.cnblogs.com","https://blog.csdn.net","https://m.gmw.cn","http://www.ce.cn"]
    po=Pool(4)  #创建进程池,开启4个进程。这里进程就为开启状态,所以后续不用start操作了。
    lis=[]
    for i in url:
        ret=po.apply_async(func1,args=(i,)) #apply_async(),异步执行,开启的是守护进程,主进程代码执行完,守护进程也结束,所以要加上close与join等待返回。
        lis.append(ret)
    time.sleep(3)
    po.close()  #关闭进程池,不再向进程池中添加新的任务
    po.join()   #与close或terminate一起使用,等待进程池中任务执行完成

    print([i.get() for i in lis])   #这里使用了get()获取返回结果,所以这里即使不使用close与join,也会等待子进程执行完成。

# 输出:
http://www.baidu.com
https://www.cnblogs.com
https://blog.csdn.net  
https://m.gmw.cn    #暂停1s     
http://www.ce.cn    #暂停1s
[('http://www.baidu.com', 200), ('https://www.cnblogs.com', 200), ('https://blog.csdn.net', 200), ('https://m.gmw.cn', 200), ('http://www.ce.cn', 200)]

回调函数的使用:获取多个网页内容,并写入文件

from multiprocessing import Pool
import os
import requests

def func1(url,):
    res=requests.get(url)
    if res.status_code == 200:
        return url,res.text

def func2(get_back):  #回调函数 将func1返回的网页url及内容写入文件
    url,content=get_back
    print("回调函数",os.getpid())
    with open("url_content.txt","a",encoding="utf-8") as f:
        f.write(url+content)
    # print(url,content)

if __name__=="__main__":
    url=["http://www.baidu.com","https://www.cnblogs.com","https://blog.csdn.net","https://m.gmw.cn","http://www.ce.cn"]
    po=Pool(3)
    lis=[]
    for i in url:
        ret=po.apply_async(func1,args=(i,),callback=func2) #apply_async(),支持callback回调函数,将进程的返回作为参数传入回调函数,对数据做进一步处理
        lis.append(ret)    #回调函数由进程池中进程的父进程调用

    po.close()  #关闭
    po.join()  #等待进程池进程执行完成
    print("父进程:",os.getpid())
输出:
回调函数 19156
回调函数 19156
回调函数 19156
回调函数 19156
主进程: 19156

线程

理论

线程与进程的关系就好像车间与流水线。车间负责资源的整合,流水线负责执行任务,一个流水线肯定属于一个车间,一个车间至少有一条流水线。车间工作的过程就是进程。

进程是资源分配的基本单位,一个进程内的所有线程共享该进程的资源。

线程是可执行的基本单位,是cpu调度的基本单位。

创建线程的开销要远小于进程,线程的创建无需申请空间。

进程之间的竞争关系,他们相互竞争计算机资源,也有协同关系,某些进程可能协同完成一个任务;
线程之间是协作关系,协同完成进程中的任务。

线程与进程的区别:

  • 线程共享创建它的进程的资源(地址空间与数据);进程有自己的资源
  • 同一进程内的线程之间可以通信;进程必须使用进程间通信来与兄弟进程通信。
  • 线程切换更快,创建线程的开销远小于进程创建的开销,新进程需要复制父进程。
  • 对主线程的更改(取消、优先级更改等)可能会影响进程其他线程的行为;对父进程的更改不会影响子进程。
  • 进程由数据段、代码段、PCB组成;
    线程由数据段、代码段、TCB组成。
  • 守护进程要么正常执行结束,要么随着父进程代码的执行完成而结束;
    守护线程要么正常执行结束,要么随着随着父线程的执行结束(包括父线程代码执行时间与普通子线程等待时间)而结束。
  • 同一程序中,可以多进程并行,因为GIL的存在,Cpython中线程不能并行
  • 在Cpython中,IO密集适用多线程,计算密集适用多进程

为什么cpython中IO密集适用多线程,计算密集适用多进程?

因为多线程场景下某一个线程IO阻塞时,cpu还可以为另一线程使用,提高效率;
计算密集时,需要cpu进行大量运算,使用多进程让多核cpu同时工作,提高效率。

注: 除了python之外的一些语言中,同一进程中的多线程是可以并行运行在多核cpu上的。进程和线程只是创建资源不同,cpu调度是一样的。

全局解释锁(GIL):在cpython中,每一个进程都有一个GIL锁,进程中每个线程在执行任务时都要先获取GIL,保证同一时间内只有一个线程在运行,防止多线程同时竞争程序中的全局变量造成线程安全,数据紊乱的问题。这是之前植入的一个机制,造成了同一进程内多线程不能并行的问题,但由于cpython的很多功能都依赖于这种机制,所以还无法移除。

创建线程及常用方法与属性

两种开启线程的方式

# 1. 普通方法,使用模块:
from threading import Thread

def func():
    print("这是一个子线程")

if __name__=="__main__":
    thr=Thread(target=func,args=())
    thr.start()
	
# 2. 使用自定义类:
from threading import Thread
class mythread(Thread):
    def __init__(self):
        super().__init__()
    
    def run(self):
        print("这是一个子线程")

thr=mythread()
thr.start()

属性与方法:

import os
import time
from threading import Thread,currentThread
# currentThread 获取当前线程信息,包括name与线程id
# 可使用currentThread().name获取线程名,currentThread().ident获取线程id

def func(i):
    time.sleep(1)
    print(i,"这是一个子线程")
    print("进程pid:",os.getpid())     #因为都在同一个进程内,无论哪里看到的进程号都一样
    print(currentThread())

if __name__=="__main__":
    for i in range(3):
        thr=Thread(target=func,args=(i,))
        thr.start()
    # time.sleep(1)
    print(thr.is_alive())  #获取thr线程的状态
    print("进程pid:",os.getpid())
    print("主线程:",currentThread())  #父线程代码结束后,还会等待子线程执行完成

#输出:
True
进程pid: 20656
主线程: <_MainThread(MainThread, started 15532)>
2 这是一个子线程
1 这是一个子线程
进程pid: 20656
进程pid: 20656
<Thread(Thread-3, started 25676)>
<Thread(Thread-2, started 26264)>
0 这是一个子线程
进程pid: 20656
<Thread(Thread-1, started 23760)>

正常情况下子线程与父线程是异步模式,想要变为同步,子线程执行完再执行父线程剩下的代码,使用join

from threading import Thread

def func(i):
    time.sleep(2)
    print(i)

if __name__=="__main__":
    lis=[]
    for i in range(3):
        thr=Thread(target=func,args=(i,))
        lis.append(thr)
        thr.start()
    [i.join() for i in lis]
    print("父线程代码结束")
	
输出:
0
1
2
父线程代码结束

守护线程

import time
from threading import Thread

def func1(i,):
    time.sleep(1)
    # time.sleep(3)
    print(i)

def func2(i,):
    time.sleep(2)
    print(i)

if __name__=="__main__":
    t1=Thread(target=func1,args=(1,))  #t1为守护线程,当父线程代码结束以及普通子线程结束后,守护线程就结束
    t1.daemon=True                     #和守护进程不一样,守护进程随父进程代码结束就结束
    t2=Thread(target=func2,args=(2,)) 
    t2.start()   
    t1.start()

    print("主线程代码结束")

总结:

开启线程的两种方式:
1. 普通方法,使用模块
2. 自定义类

threading.currentThread()         获取当前线程信息,包括name与线程id
threading.currentThread().name    获取当前线程名字
threading.currentThread().ident   获取当前线程id

t=rhreading.Thread(target=func,args=(i,)) 定义一个子线程
t.daemon=True  将t设置为守护进程,不做特殊设定的情况下,父线程结束(父线程代码以及普通子线程)后,守护线程就结束。
t.start() 开启子线程
t.join() 等t子线程执行完成再继续执行父线程的代码
t.is_alive() 查看线程t的状态

线程可以共享进程中的变量与属性,所以一些需要多线程共享的资源可以直接使用,无需传值。

锁机制

递归锁(Rlock):一把万能钥匙,可以开无止境的锁,但钥匙一次只能一个人获取。
互斥锁(Lock):一把钥匙配一把锁。只有还了钥匙才能开下一把锁。
GIL:是CPython解释器上的一个锁,锁的是线程,意思是在同一进程中,同一时间只允许一个线程访问cpu。

递归锁的简单使用:

from threading import RLock,Thread
import time
def func():
    l.acquire()    
    time.sleep(1)  
    #正常情况下父线程与子线程同时运行,但子线程拿到了锁与钥匙,
    #父线程若要用这把锁就得等子线程还回去
    print("子线程",123)
    l.acquire()
    print("子线程",456)
    l.acquire()
    print("子线程",789)
    l.release()   #递归锁可以一把钥匙开很多层锁,但最后所有的acquire都要有对应的release
    l.release()
    l.release()

l=RLock()

t=Thread(target=func)
t.start()

l.acquire()
print("父线程",123)
l.acquire()
print("父线程",456)
l.acquire()
print("父线程",789)
l.release()
l.release()
l.release()

输出:
(sleep 1s) 父线程 123
父线程 456
父线程 789
子线程 123
子线程 456
子线程 789

**with lock: ** 自动添加&释放锁

from threading import RLock,Thread
import time

def func():
    print("a")

    #  with lock: 自动加锁,执行完后with中的代码会自动解锁,
    # 虽然print('b')没有在锁的范围内,但是也要等上面代码执行完才能执行下面的代码
    with lock:
        time.sleep(1)
        print(123)
    print('b')

    # 相当于:
    # lock.acquire()
    # time.sleep(1)
    # print(123)
    # lock.release()
    print('b')

if __name__=="__main__":
    lock=RLock()
    for i in range(10):
        Thread(target=func).start()

很多时候由于用锁不当,容易造成死锁,例如用互斥锁的这个情况:

# 两个人去厕所,小雪抢到了厕所,铁柱拿到了卫生纸,这两个资源都不能释放,造成了死锁。

from threading import Thread,RLock,Lock
import time
def first_person(name):
    lto.acquire()
    print("%s抢到了%s" %(name,tolet))
    time.sleep(1)
    lpa.acquire()
    print("%s抢到了%s" %(name,paper))
    lpa.release()
    lto.release()

def second_person(name):
    lpa.acquire()
    print("%s抢到了%s" %(name,paper))
    time.sleep(1)
    lto.acquire()
    print("%s抢到了%s" %(name,tolet))
    lto.release()
    lpa.release()

paper="卫生纸"
tolet="厕所"
lto=Lock()
lpa=Lock()
thr1=Thread(target=first_person,args=("小雪",))
thr2=Thread(target=second_person,args=("铁柱",))
thr1.start()
thr2.start()

对于上面的例子,正确的情况应该是一个人使用完两个资源,释放后,再第二个人使用。
可以使用递归锁Rlock解决:

from threading import Thread,RLock,Lock
import time
def first_person(name):
    lto.acquire()
    print("%s抢到了%s" %(name,tolet))
    time.sleep(1)
    lpa.acquire()
    print("%s抢到了%s" %(name,paper))
    lpa.release()
    lto.release()

def second_person(name):
    lpa.acquire()
    print("%s抢到了%s" %(name,paper))
    time.sleep(1)
    lto.acquire()
    print("%s抢到了%s" %(name,tolet))
    lto.release()
    lpa.release()

paper="卫生纸"
tolet="厕所"
lto=lpa=RLock()
thr1=Thread(target=first_person,args=("小雪",))
thr2=Thread(target=second_person,args=("铁柱",))
thr1.start()
thr2.start()

输出:
小雪抢到了厕所
小雪抢到了卫生纸
铁柱抢到了卫生纸
铁柱抢到了厕所

信号量

一个锁配自定义把钥匙,同时供自定义次调用。

from threading import Thread,Semaphore
import time
def func(i):
    sem.acquire()
    time.sleep(2)
    print("第%s个人拿到钥匙走进房间" %i)
    sem.release()
if __name__=="__main__":
    sem=Semaphore(3)     #一把锁配3把钥匙,一次允许3个人使用
    for i in range(8):
        t=Thread(target=func,args=(i,))
        t.start()

输出:
(sleep 2s)
第2个人拿到钥匙走进房间
第1个人拿到钥匙走进房间
第0个人拿到钥匙走进房间 (sleep 2s)
第3个人拿到钥匙走进房间
第4个人拿到钥匙走进房间
第5个人拿到钥匙走进房间 (sleep 2s)
第7个人拿到钥匙走进房间
第6个人拿到钥匙走进房间

事件

线程的事件和进程的事件用法一样。
event实际上描述的是一种同步的处理事件的方式,可以简单地理解为,不同的线程之间可以利用一些特殊的标识来等待其他进程的某些程序处理完毕。
多个线程都拥有同一个event示例,当某个线程调用 实例.wait()方法时,为阻塞状态。当其他线程的阻塞标识切换为True时,同一时间点 实例.wait()的状态切换为非阻塞。

e=Event()
e.is_set() #阻塞标识。返回true或False,默认为False
e.wait() #当e.is_set()阻塞标识的值为False,e.wait()这里就会阻塞,反之则非阻塞。
e.set() #将is_set()设为True
e.clear() #将is_set()设为False

红绿灯

from threading import Thread,Event
import time

def func():
    while 1:
        e.wait()
        print("车走了")
        time.sleep(1)
if __name__=="__main__":
    e=Event()

    t=Thread(target=func,args=())
    t.start()
    print("红灯亮")
    while 1:
        time.sleep(3)
        e.set()
        print("绿灯亮")
        time.sleep(3)
        e.clear()
        print("红灯亮")

条件

条件不仅提供类似于Lock锁的机制,还提供类似于事件的阻塞机制。

con=Condition() #实例化一个条件对象
con.notify(num) #释放num个线程为非阻塞
con.notifyAll() #notifyAll()将全部线程的wait变为非阻塞。
con.wait()   #这里默认为阻塞,notify之后部分线程为非阻塞

con.acquire()  上锁
con.release()  解锁

注: notify,wait 必须与上锁解锁一起用,锁必须释放之后才能在另一个地方使用。

使用示例

from threading import Thread,Condition,Lock
import time

def func(i):
        con.acquire()
        con.wait()
        # con.release()  #如果release在这里,那么三个线程的输出就会同时出来。
        print("第%s辆车走了"%i)
        time.sleep(1)
        con.release()  #根据输出我们发现notify(3),释放的三个线程却是一个执行完再执行了另一个,这是为什么呢?是因为这里的整段代码都被锁住了,必须要上一个线程释放,下一个线程才能拿到锁。

if __name__=="__main__":
    con=Condition()
    for i in range(10):
        t=Thread(target=func,args=(i,))
        t.start()

    while 1:
        con.acquire()
        print("主线程拿到锁")
        con.notify(3)
        # con.notifyAll() #notifyAll()将全部线程的的wait变为非阻塞。
        print("主线程释放锁")
        con.release()
        time.sleep(5)

输出:
image

定时器

t=Timer(num,func) 实例化一个num s后执行func的计时器
t.start() #启动计时器,新开一个普通线程执行func,与父线程为异步状态,

from threading import Timer

def func():
    print("执行")

t=Timer(2,func)  #实例化一个定时器
# time.sleep(2)
t.start()  #启动定时器,在这里sleep 2s,执行func

线程池

在线程池中开启固定数量的线程,有任务时会自动执行,提交任务后子线程与主线程默认为异步关系。
需要注意: 因为GIL的存在,多线程并不是真正的并行。所以大量计算的任务用进程池会更合适。
线程池的方法

from concurrent.futures import ThreadPoolExecutor
t_p=ThreadPoolExecutort(num) #开启num个线程的线程池
t_p.submit(func,参数1,参数2) #提交任务给线程,执行func函数,参数1,参数2是func传入的参数
t_p.submit(func,参数1,参数2).add_done_callback(回调函数名) #提交任务给线程,将线程的返回值传给回调函数处理。回调函数由线程池中的子线程执行,这是和进程不一样的地方,进程的回调函数由父进程执行。
t_p.map(func,迭代器) 相当于 for+submit
t_p.shutdown()  #相当于进程池中的 close+join,是指不允许再继续向池中增加任务,然后让父线程等待池中所有进程执行完所有任务

concurrent.futures也有线程池
这里将之前学的多进程模块的进程池与concurrent.futures的线程池与进程池使用一个计算量大的例子进行效率比较

from concurrent.futures import ThreadPoolExecutor
import time
def func(i):
    sum = 0
    for j in range(i):
        for z in range(j):
            for x in range(z):
                sum+=x
    print(sum)

if __name__=="__main__":
    t_p=ThreadPoolExecutor(8)
    a=time.time()
    for i in range(2000):
        t_p.submit(func,i)
    t_p.shutdown()  #相当于进程池中的 close+join 
    print(time.time()-a)
    print("父线程代码执行结束") 
#执行完成用了90s


from concurrent.futures import ProcessPoolExecutor
import time
def func(i):
    sum = 0
    for j in range(i):
        for z in range(j):
            for x in range(z):
                sum+=x
    print(sum)

if __name__=="__main__":
    p_p=ProcessPoolExecutor(8)
    a=time.time()
    for i in range(500):
        p_p.submit(func,i)
    p_p.shutdown()  #相当于进程池中的 close+join 
    print(time.time()-a)
    print("父进程代码执行结束")
#执行完成用了约24s


from multiprocessing import Pool
import time
def func(i):
    sum = 0
    for j in range(i):
        for z in range(j):
            for x in range(z):
                sum+=x
    print(sum)

if __name__=="__main__":
    p_p=Pool(8)
    a=time.time()
    for i in range(500):
        p_p.apply_async(func,args=(i,))
    p_p.close()
    p_p.join()
    print(time.time()-a)
    print("父进程代码执行结束")
#执行完成用了约24s

可以看到,两种方式的进程池花费时间差不多,线程池做计算量大的任务明显慢很多。

submit+回调函数的使用

from concurrent.futures import ThreadPoolExecutor
from threading import currentThread

def func(i):
    sum = 0
    for j in range(i):
        sum+=j
    return sum

def call_back(ret):
    print("回调函数中:",ret.result(),currentThread())   #ret.result()为函数func返回的值
if __name__=="__main__":
    t_p=ThreadPoolExecutor(8)
    for i in range(20):
        t_p.submit(func,i).add_done_callback(call_back) #将任务submit提交给线程处理,将返回结果传给回调函数,具体的返回结果需要用result获取
    t_p.shutdown()  #相当于进程池中的 close+join 

    print("父线程代码执行结束",currentThread()) 
	
输出:
回调函数中: 0 <_MainThread(MainThread, started 26332)>  #可以看到回调函数由线程池中的子线程执行
回调函数中: 0 <Thread(ThreadPoolExecutor-0_0, started 28844)>
回调函数中: 1 <_MainThread(MainThread, started 26332)>
回调函数中: 3 <Thread(ThreadPoolExecutor-0_1, started 28732)>
回调函数中: 6 <Thread(ThreadPoolExecutor-0_0, started 28844)>
回调函数中: 10 <_MainThread(MainThread, started 26332)>
回调函数中: 15 <Thread(ThreadPoolExecutor-0_1, started 28732)>
回调函数中: 21 <Thread(ThreadPoolExecutor-0_2, started 16916)>
回调函数中: 36 <_MainThread(MainThread, started 26332)>
回调函数中: 28 <Thread(ThreadPoolExecutor-0_0, started 28844)>
父线程代码执行结束 <_MainThread(MainThread, started 26332)>

多任务的提交:map的使用

from concurrent.futures import ThreadPoolExecutor
import time
def func(i):
    sum = 0
    for j in range(i):
        for z in range(j):
            for x in range(z):
                sum+=x
    print(sum)

if __name__=="__main__":
    t_p=ThreadPoolExecutor(8)
    a=time.time()
    t_p.map(func,range(2000))  #使用map相当于for + submit
    t_p.shutdown()  #相当于进程池中的 close+join 
    print(time.time()-a)
    print("父线程代码执行结束") 

map还可将每一个线程的返回值生成一个迭代器

from concurrent.futures import ThreadPoolExecutor

def func(i):
    sum = 0
    for j in range(i):
        sum+=j
    return sum

if __name__=="__main__":
    t_p=ThreadPoolExecutor(8)
    ret=t_p.map(func,range(200))  #使用map相当于for + submit,将各线程的返回值生成一个迭代器
    t_p.shutdown()  #相当于进程池中的 close+join 

    print(ret.__next__())  #获取迭代器中的一个值
    print(ret.__next__())
    print(ret.__next__())

    print("父线程代码执行结束") 

协程

之前学习了进程与线程提高cpu利用率的方法,这里再介绍一种协程。它能基于单线程实现并发,相比之下能节约创建进程或线程的开销。
并发的本质就是: 切换+保存状态,即任务之间的切换与切换时任务状态的保存,方便下一次切回来继续执行。

我们知道在cpu运行一个任务时有两种情况会切换:

  1. 任务因为IO的原因发生阻塞;
  2. 任务计算时间过长,cpu时间片用完。

在第一种情况下,使用协程能提高执行效率,第二种情况不适用于协程。

这里用一幅图来解释协程:
image

之前学过的yield就有单纯的切换与保存状态的功能

def consumer():
    while 1:
        x=yield
        print(x)

def producer():
    con=consumer()
    next(con)     
    for i in range(5):
 #这里由producer切换到consumer,再回到producer执行时,仍会知道循环到了哪里,这就是切换与保存状态
        con.send(i) 

producer()

但是yield并不能实现遇到在一个函数遇到IO阻塞时,切换到另一个函数执行。

greenlet模块

greenlet模块提供了更简单的实现函数与函数之间的切换的方法,但遇到IO操作仍会阻塞,并不能提高效率。

from greenlet import greenlet
import time

def eat(name):
    print("%s 吃炸鸡" %name)
    time.sleep(2)
    f2.switch("大柱")
    print("%s 吃蛋挞" %name)
    f2.switch()

def drink(name):
    print("%s 喝啤酒" %name)
    time.sleep(2)
    f1.switch()
    print("%s 喝可乐" %name)

a=time.time()
f1=greenlet(eat)  #greenlet(函数) 将函数封装为greenlet对象f1,我理解为装饰器,给函数封装一些功能。
f2=greenlet(drink)
f1.switch("小雪") #f1.switch 执行f1对应的函数,并传参。遇到f2.witch时执行f2对应的函数,并保存f1的执行状态。
print(time.time()-a)

输出:
小雪 吃炸鸡
大柱 喝啤酒
小雪 吃蛋挞
大柱 喝可乐
4.015383005142212

gevent模块

可以用来创建协程对象,实现在某一个函数中遇到IO阻塞时,自动切换到其他函数执行。
Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。

常用方法
g1=gevent.spawn(func,1,,2,3,x=4,y=5)创建一个协程对象g1,spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数eat的

g2=gevent.spawn(func2)

g1.join() #等待g1结束

g2.join() #等待g2结束

#或者上述两步合作一步:gevent.joinall([g1,g2])

g1.value#拿到func1的返回值

使用举例:

import gevent 
import time

def eat(name):
    print("%s 吃炸鸡" %name)
    gevent.sleep(2)            #gevent只能识别gevent中有的阻塞,也可以添加补丁,gevent就能自动识别大多数阻塞,后续会讲到
    print("%s 吃蛋挞" %name)

def drink(name):
    print("%s 喝啤酒" %name)
    gevent.sleep(2)
    print("%s 喝可乐" %name)

a=time.time() 
g1=gevent.spawn(eat,"小雪")     #创建协程对象g1,执行eat函数,给eat传参"小雪"
g2=gevent.spawn(drink,"大柱")   #创建协程对象g2,当一个协程对象阻塞时,就会执行另一个协程对象。
# g1.join()  #等待g1执行完成
# g2.join()  #等待g2执行完成
gevent.joinall([g1,g2])   #等待所有的协程对象执行完成
print(time.time()-a)

输出:
小雪 吃炸鸡
大柱 喝啤酒
小雪 吃蛋挞
大柱 喝可乐
2.032289743423462  #正常执行此程序需要4s,使用协程后只需要2s

上例gevent.sleep(2)模拟的是gevent可以识别的io阻塞,而time.sleep(2)或其他的阻塞,gevent是不能直接识别的需要用下面一行代码,打补丁,就可以识别了

from gevent import monkey;monkey.patch_all()必须放到被打补丁者的前面,如time,socket模块之前

或者我们干脆记忆成:要用gevent,需要将from gevent import monkey;monkey.patch_all()放到文件的开头
上面的代码就可以改写成:

from gevent import monkey;monkey.patch_all() #加上此行
import gevent 
import time

def eat(name):
    print("%s 吃炸鸡" %name)
	print(threading.currentThread())  #查看当前线程信息
    time.sleep(2)
    print("%s 吃蛋挞" %name)

def drink(name):
    print("%s 喝啤酒" %name)
	print(threading.currentThread()) #查看当前线程信息
    time.sleep(2)
    print("%s 喝可乐" %name)

a=time.time() 
g1=gevent.spawn(eat,"小雪")
g2=gevent.spawn(drink,"大柱")
# g1.join()  #等待g1执行完成
# g2.join()  #等待g2执行完成
gevent.joinall([g1,g2])
print(time.time()-a)

输出: 
小雪 吃炸鸡
<_DummyThread(Dummy-1, started daemon 2517095163824)> #DummyThread 假线程
大柱 喝啤酒
<_DummyThread(Dummy-2, started daemon 2517095164096)>
小雪 吃蛋挞
大柱 喝可乐
2.0294768810272217

总结:多进程、多线程、协程使用场景

计算密集场景使用多进程,可以充分利用多核cpu;
IO密集场景使用多线程。
协程是属于单线程中的一种执行过程。
协程由程序员调度,而多线程由操作系统调度。

IO多路复用

这里讨论的背景是Linux环境下的network IO,分为5种IO:

  • blocking IO 阻塞IO
  • nonblocking IO 非阻塞IO
  • IO multiplexing IO多路复用
  • signal driven IO 信号驱动IO(不常用)
  • asynchronous IO 异步IO

对于一个network IO(以read举例),它会涉及到两个系统对象,调用这个IO的进程(或者说线程),以及系统内核,一个read操作主要经历以下两个阶段:

  • 等待准备数据
  • 将数据从内核拷贝到进程中

Linux内核事件机制

linux内核将阻塞的进程(线程)任务添加到等待数据队列中,通过遍历整个等待数据队列,将未就绪的任务休眠,将处于就绪状态的任务唤醒,并将其从等待队列中删除,并通过节点上的回调函数通知进程已就绪,然后加入到cpu就绪队列中等待cpu调度执行。整个过程主要包括两部分: 休眠逻辑与唤醒逻辑

休眠逻辑

  • 在linux内核中某一个进程任务task执行需要等待某个条件condition被触发执行之前,首先会在内核中创建一个等待节点entry,然后初始化entry相关属性信息,其中将进程任务存放在entry节点并同时存储一个wake_callback函数并挂起当前进程
  • 其次不断轮询检查当前进程任务task执行的condition是否满足,如果不满足则调用schedule()进入休眠状态
  • 最后如果满足condition的话,就会将entry从队列中移除,也就是说这个时候事件已经被唤醒,进程处于就绪状态

唤醒逻辑

  • 在等待队列中循环遍历所有的entry节点,并执行回调函数,直到当前entry为排他节点的时候退出循环遍历
  • 执行的回调函数中,存在私有逻辑与公用逻辑,类似模板方法设计模式
  • 对于default_wake_function的唤醒回调函数主要是将entry的进程任务task添加到cpu就绪队列中等待cpu调度执行任务task

阻塞IO

使用socket,默认都是阻塞的,下图是udp的recvfrom,该进程问内核要数据,内核会先将其放到等待队列中让其处于休眠状态,如果数据还在网络传输中,内核会等待数据传输完成,待数据准备好后,再将数据从内核拷贝到进程空间,再遍历等待队列,将其唤醒。
image

当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据。对于network io来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。
而用户进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。

blocking IO的特点就是在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被block了。

问题: 在网络编程中,单进程或单线程运行一个程序,遇到阻塞IO,进程或线程只能等待返回或超时,期间无法执行任何运算或响应网络请求,效率低下。

方案一: 
使用多进程或多线程,每一个连接单独使用一个进程/线程,互不影响。
此方案有一个问题,当连接数较多时,开启拿到进程或线程太多,严重占据系统资源,造成系统的反应过慢,程序假死等问题。

方案二:
使用线程池与连接池。将线程数维持在合理的数量,省去创建与销毁线程的开销,线程空闲后会继续执行新的任务;连接池可以保持一定数量的长连接,重用已有的连接,减少断开与关闭连接的频率。
此方案普遍用于中小规模的请求场景中,当请求数更多时,也会遇到瓶颈。

方案三:
尝试用非阻塞IO解决大规模请求的问题。

非阻塞IO

image
从图中可以看出,当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是用户就可以在本次到下次再发起read询问的时间间隔内做其他事情,或者直接再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存(这一阶段仍然是阻塞的),然后返回。

所以,在非阻塞式IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有,以下是非阻塞IO代码的演示:

#server端:
import socket
import time

sk=socket.socket()
sk.setblocking(False)  #将其设置为非阻塞
sk.bind(("127.0.0.1",8081))
sk.listen()

del_lis=[]
conn_lis=[]

while 1:
    time.sleep(2)
    try:
        conn,addr=sk.accept()   #非阻塞模式下,accept()没有客户端连接时会报错BlockingIOError
        conn_lis.append(conn)   #将客户端连接的socket套接字放在列表中,以便后续获取信息
    
    except BlockingIOError:  
        if conn_lis:     
            for i in conn_lis:  
                try:                   #频繁地recv与内核交互,将占用大量资源,所以while下会time.sleep(1)
                    msg_r=i.recv(1024).decode("utf-8")   #非阻塞模式下,recv()没有接收到信息时会报错BlockingIOError
                    if not msg_r:
                        print("一个客户端关闭连接")
                        del_lis.append(i)
                        i.close()
                    else:
                        print("接收消息:",msg_r)
                        i.send(msg_r.upper().encode("utf-8"))
                except ConnectionResetError:  #客户端强制断开的报错
                    i.close()
                except BlockingIOError: #非阻塞Io没有数据的
                    pass
        
    if del_lis:
        for i in del_lis:
            conn_lis.remove(i)
        del_lis.clear()
--------------------------------------------------------
#client 端:
import socket

sk=socket.socket()
sk.connect(("127.0.0.1",8081))
while 1:
    msg=input(">>>")
    if msg=="q":
        break
    else:
        sk.send(msg.encode("utf-8"))
        print(sk.recv(1024).decode("utf-8"))

sk.close()

但是非阻塞IO并不被推荐使用,因为虽然在等待时间,程序也在进行其他工作,但是不断重复的recv,也大幅度提高了cpu占用率,任务完成的延迟也增大了,因为每隔一段时间才会进行read操作。

此外,在这个方案中recv()更多的是起到检测“操作是否完成”的作用,实际操作系统提供了更为高效的检测“操作是否完成“作用的接口,例如select()多路复用模式,可以一次检测多个连接是否活跃。

多路复用IO

image

以上是IO复用模型图,一个进程可以处理多个socket描述符的操作。当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,为可读状态,select就会返回对应的socket。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。

这里的复用是指实现一个进程处理任务能够接收N个socket并对这N个socket进行操作的技术。
复用
image

  • 用户进程向内核发起select函数的调用,并携带socket描述符集合从用户空间复制到内核空间,由内核对socket集合进行可读状态的监控.
  • 其次当前内核没有数据可达的时候,将注册的socket集合分别以entry节点的方式添加到链表结构的等待队列中等待数据报可达.
  • 这个时候网卡设备接收到网络发起的数据请求数据,内核接收到数据报,就会通过轮询唤醒的方式(内核并不知道是哪个socket可读)逐个进行唤醒通知,直到当前socket描述符有可读状态的时候就退出轮询然后从等待队列移除对应的socket节点entry,并且这个时候内核将会更新fd集合中的描述符的状态,以便于用户进程知道是哪些socket是具备可读性从而方便后续进行数据读取操作
  • 同时在轮询唤醒的过程中,如果有对应的socket描述符是可读的,那么此时会将read_process加入到cpu就绪队列中,让cpu能够调度执行read_process任务
  • 最后是用户进程调用select函数返回成功,此时用户进程会在socket描述符结合中进行轮询遍历具备可读的socket,此时也就意味着数据此时在内核已经准备就绪,用户进程可以向内核发起数据读取操作,也就是执行上述的read_process任务操作

常见的IO复用模型有三种:select poll 与 epoll

select

r,w,x=select.select(rlist,wlist,xlist,timeout)
第一个参数是我们需要监听可读的套接字, 第二个参数是我们需要监听可写的套接字, 第三个参数使我们需要监听异常的套接字, 第四个则是时间限制设置。列表传入后,select会开始监听,当某一个套接字有动静后,会以列表的形式返回对应是套接字。
代码演示:

服务端:
import select
import socket

sk=socket.socket()
sk.bind(("127.0.0.1",8081))
sk.listen()

r_list=[sk]

while 1:
    r,w,x=select.select(r_list,[],[])  #这里会阻塞,只有列表中的socket fd有返回时,才会将对应的socket fd返回

    if r:
        for i in r:
            if i == sk:     #当有返回的是sk时,则表示有客户端连接,将建立连接的套接字也放到 r_list,以便select监控 
                conn,addr=sk.accept()
                r_list.append(conn)
            else:
                try:
                    msg_r=i.recv(1024).decode("utf-8")  #当有返回的是建立连接的套接字对象,分为两种情况:对方关闭连接与正常发送了信息
                    if not msg_r:   #客户端主动/强制关闭连接,则将对应套接字从select监控列表中删除
                        print("一个客户端关闭连接")
                        i.close()
                        r_list.remove(i)  #从r_list删除关闭的连接
                    else:          #正常接收了信息,将接收的内容转为大写发送回去
                        print(msg_r)
                        i.send(msg_r.upper().encode("utf-8"))
                except ConnectionResetError:# 如果客户端强制断开连接而引发的异常
                    print("一个客户端强制关闭连接")
                    i.close()# 此时就把强制断开连接的客户端的连接,关闭
                    r_list.remove(i) #从r_list删除关闭的连接

----------------------------------------------------
客户端:
import socket

sk=socket.socket()
sk.connect(("127.0.0.1",8081))
while 1:
    msg=input(">>>")
    if msg=="q":
        break
    else:
        sk.send(msg.encode("utf-8"))
        print(sk.recv(1024).decode("utf-8"))

sk.close()

开启服务端后,可以和多个客户端同时通信

select技术最大可支持的描述符个数为1024个;
用户进程调用select的时候需要将一整个fd集合的大块内存从用户空间拷贝到内核中,再加上调用select的频率本身非常频繁,这样导致高频率调用且大内存数据的拷贝,严重影响性能;
最后唤醒逻辑的处理,select技术在等待过程如果监控到至少有一个socket事件是可读的时候将会遍历唤醒整个等待队列,确认对应的可读事件,消耗大量资源。
select也支持windows,其他两个模型不支持windows。

poll

poll技术与select技术实现逻辑基本一致,重要区别在于其使用链表的方式存储描述符fd,没有fd的个数限制。

epoll

epoll解决了 select与poll的两个性能问题。
对于大内存数据的拷贝问题,epoll创建了epoll空间用于存放所有socket事件,事件的增删该都是在该空间进行,用户空间与内核空间对该epoll空间是可见的,就避免了占用内核空间与相互拷贝的问题。

提问:三种IO模型的区别?

异步IO

image
用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。

注:
IO多路复用参考文档:https://cloud.tencent.com/developer/article/1596048

posted @ 2021-09-27 18:16  huandada  阅读(117)  评论(0编辑  收藏  举报