Python之网络编程与并发编程(详细)
文章目录
一、基于tcp/udp协议的socket套接字编程
1、套接字工作流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-01za27d1-1635731071028)(C:\Users\pilgirm\AppData\Roaming\Typora\typora-user-images\image-20210719155223101.png)]
2、套接字的部分函数使用方法及其含义
2.1、服务端套接字函数
方法 | 用途 |
---|---|
server.lind(ip地址,端口号) | 绑定(ip地址,端口号)到套接字 |
server.listen(整数) | 设置半连接池大小,开启tcp监听 |
server.accept() | 被动接受TCP客户的连接,(阻塞式)等待连接的到来 |
server.recv() | 接收TCP数据 |
2.2、客户端套接字函数
方法 | 用途 |
---|---|
s.connect(ip地址,端口号) | 主动初始化tcp服务器连接 |
s.connect_ex(ip地址,端口号) | connect()函数的扩展版本,出错时返回出错码,而不是抛出异常 |
3.3、公共用途的套接字函数
方法 | 用途 |
---|---|
s.recv() | 接收TCP数据 |
s.send(数据) | 发送tcp数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完) |
s.send(数据) | 发送完整的tcp数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完) |
s.recvfrom() | 接受udp数据 |
s.sendto(数据) | 发送udp数据 |
s.getpeername() | 连接到当前套接字的远端的地址 |
s.getsockname() | 当前套接字的地址 |
s.getsockopt() | 返回指定套接字的参数 |
s.setsockopt() | 设置指定套接字的参数 |
s.close() | 关闭套接字 |
3.4、面向锁的套接字方法
方法 | 用途 |
---|---|
s.setblocking() | 设置套接字的阻塞与非阻塞模式 |
s.settimeout() | 设置阻塞套接字操作的超时时间 |
s.gettimeout() | 得到阻塞套接字操作的超时时间 |
3.5、面向文件的套接字函数
方法 | 用途 |
---|---|
s.fileno() | 套接字的文件描述符 |
s.makefile() | 创建一个与该套接字相关的文件 |
3、基于tcp通信协议的代码
3.1、服务端
import socket
"""
服务端
"""
# 第一个socket是模块名,第二个是类名
# SOCK_STREAM => 基于tcp传递 在此设置为tcp协议
server = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)
# bind方法绑定ip和端口
server.bind(('127.0.0.1',8080))
# 设置半连接池大小
server.listen(6)
print('正在接受用户输入:')
while True:
# 等待用户的输入
sock, addr = server.accept()
print(sock)
print(addr)
while True:
# 用户非正常状态关闭客户端,捕获异常
try:
# 设置收/发的最大字节数
data = sock.recv(1024)
print(data.decode('utf-8'))
if len(data) == 0:
break
# 发送数据
sock.send(data.upper())
except Exception as e:
print(e)
break
# 客户端关闭
# print('客户端关闭')
sock.close()
# 服务端关闭
server.close()
print('服务端关闭')
3.2、客户端
import socket
"""
客户端
"""
# 在此设置为tcp协议
client = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)
# 连接ip和端口
client.connect(('127.0.0.1',8080))
while True:
# 发送信息
v = input('>>(q to quit):').strip()
if not v:continue
if v == 'q':
print('quit')
break
client.send(v.encode('utf-8'))
# 设置收/发的最大字节数
data = client.recv(1024)
print(data.decode('utf-8'))
# continue
client.close()
4、基于udp通信协议的代码
4.1、服务端
"""
基于udp协议建立连接
"""
import socket
# SOCK_DGRAM => udp通信协议
servser = socket.socket(family=socket.AF_INET,type=socket.SOCK_DGRAM)
# 绑定端口
servser.bind(('127.0.0.1',8080))
# # 设置半连接池大小
# servser.listen(4)
# 与用户建立连接
# sock,addr = servser.accept()
# 接收文件的大小
while True:
data,client_addr = servser.recvfrom(1024)
if len(data) == 0:
break
print('\033[34m来自%s的一条消息:%s'%(client_addr,data.decode('utf-8')))
reply = input('请输入回复消息:')
servser.sendto(reply.encode('utf-8'),client_addr)
servser.close()
4.2、客户端
import socket
client = socket.socket(family=socket.AF_INET,type=socket.SOCK_DGRAM)
while True:
msg = input('回复消息:').strip()
if not msg:continue
if msg == 'q':
print('退出')
break
client.sendto(msg.encode('utf-8'),('127.0.0.1',8080))
data,server_addr = client.recvfrom(1024)
print('\033[34m来自%s的一条消息:%s'%(server_addr,data.decode('utf-8')))
client.close()
二、进程
1、进程的概念
- 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
- 狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。
- 广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。
2、并发和并行的区别
并发:也就是在一个精确的时间片刻,有不同的程序在执行,这就要求必须有多个处理器。
并行:多个软件在一个时间段上真正意义上的同时运行,比如一个服务器同时处理多个session。
3、进程的调度
1、先来先服务算法
-
先来先服务(FCFS)调度算法是一种最简单的调度算法,该算法既可用于作业调度,也可用于进程调度。FCFS算法比较有利于长作业(进程),而不利于短作业(进程)。由此可知,本算法适合于CPU繁忙型作业,而不利于I/O繁忙型的作业(进程)。
2、短作业优先算法
-
短作业(进程)优先调度算法(SJ/PF)是指对短作业或短进程优先调度的算法,该算法既可用于作业调度,也可用于进程调度。但其对长作业不利;不能保证紧迫性作业(进程)被及时处理;作业的长短只是被估算出来的。
3、时间片轮转法
-
时间片轮转(Round Robin,RR)法的基本思路是让每个进程在就绪队列中的等待时间与享受服务的时间成比例。在时间片轮转法中,需要将CPU的处理时间分成固定大小的时间片,例如,几十毫秒至几百毫秒。如果一个进程在被调度选中之后用完了系统规定的时间片,但又未完成要求的任务,则它自行释放自己所占有的CPU而排到就绪队列的末尾,等待下一次调度。同时,进程调度程序又去调度当前就绪队列中的第一个进程。 显然,轮转法只能用来调度分配一些可以抢占的资源。这些可以抢占的资源可以随时被剥夺,而且可以将它们再分配给别的进程。CPU是可抢占资源的一种。但打印机等资源是不可抢占的。由于作业调度是对除了CPU之外的所有系统硬件资源的分配,其中包含有不可抢占资源,所以作业调度不使用轮转法。 在轮转法中,时间片长度的选取非常重要。首先,时间片长度的选择会直接影响到系统的开销和响应时间。如果时间片长度过短,则调度程序抢占处理机的次数增多。这将使进程上下文切换次数也大大增加,从而加重系统开销。反过来,如果时间片长度选择过长,例如,一个时间片能保证就绪队列中所需执行时间最长的进程能执行完毕,则轮转法变成了先来先服务法。时间片长度的选择是根据系统对响应时间的要求和就绪队列中所允许最大的进程数来确定的。 在轮转法中,加入到就绪队列的进程有3种情况: 一种是分给它的时间片用完,但进程还未完成,回到就绪队列的末尾等待下次调度去继续执行。 另一种情况是分给该进程的时间片并未用完,只是因为请求I/O或由于进程的互斥与同步关系而被阻塞。当阻塞解除之后再回到就绪队列。 第三种情况就是新创建进程进入就绪队列。 如果对这些进程区别对待,给予不同的优先级和时间片从直观上看,可以进一步改善系统服务质量和效率。例如,我们可把就绪队列按照进程到达就绪队列的类型和进程被阻塞时的阻塞原因分成不同的就绪队列,每个队列按FCFS原则排列,各队列之间的进程享有不同的优先级,但同一队列内优先级相同。这样,当一个进程在执行完它的时间片之后,或从睡眠中被唤醒以及被创建之后,将进入不同的就绪队列。
4、多级反馈队列
-
前面介绍的各种用作进程调度的算法都有一定的局限性。如短进程优先的调度算法,仅照顾了短进程而忽略了长进程,而且如果并未指明进程的长度,则短进程优先和基于进程长度的抢占式调度算法都将无法使用。 而多级反馈队列调度算法则不必事先知道各种进程所需的执行时间,而且还可以满足各种类型进程的需要,因而它是目前被公认的一种较好的进程调度算法。在采用多级反馈队列调度算法的系统中,调度算法的实施过程如下所述。 (1) 应设置多个就绪队列,并为各个队列赋予不同的优先级。第一个队列的优先级最高,第二个队列次之,其余各队列的优先权逐个降低。该算法赋予各个队列中进程执行时间片的大小也各不相同,在优先权愈高的队列中,为每个进程所规定的执行时间片就愈小。例如,第二个队列的时间片要比第一个队列的时间片长一倍,……,第i+1个队列的时间片要比第i个队列的时间片长一倍。 (2) 当一个新进程进入内存后,首先将它放入第一队列的末尾,按FCFS原则排队等待调度。当轮到该进程执行时,如它能在该时间片内完成,便可准备撤离系统;如果它在一个时间片结束时尚未完成,调度程序便将该进程转入第二队列的末尾,再同样地按FCFS原则等待调度执行;如果它在第二队列中运行一个时间片后仍未完成,再依次将它放入第三队列,……,如此下去,当一个长作业(进程)从第一队列依次降到第n队列后,在第n 队列便采取按时间片轮转的方式运行。 (3) 仅当第一队列空闲时,调度程序才调度第二队列中的进程运行;仅当第1~(i-1)队列均空时,才会调度第i队列中的进程运行。如果处理机正在第i队列中为某进程服务时,又有新进程进入优先权较高的队列(第1~(i-1)中的任何一个队列),则此时新进程将抢占正在运行进程的处理机,即由调度程序把正在运行的进程放回到第i队列的末尾,把处理机分配给新到的高优先权进程。
2、同步,异步,阻塞,非阻塞
1、同步和异步
-
所谓同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。要么成功都成功,失败都失败,两个任务的状态可以保持一致。
-
所谓异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了
。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,
所以它是不可靠的任务序列。
2、阻塞和非阻塞
- 阻塞和非阻塞这两个概念与程序(线程)等待消息通知(无所谓同步或者异步)时的状态有关。也就是说阻塞与非阻塞主要是程序(线程)等待消息通知时的状态角度来说的
3、同步阻塞形式
- 效率最低,什么别的事都不做
4、异步阻塞形式
- 异步操作是可以被阻塞住的,只不过它不是在处理消息时阻塞,而是在等待消息通知时被阻塞。`
5、同步非阻塞模式
- 实际上是效率低下的,这个程序需要在这两种不同的行为之间来回的切换
6、异步阻塞方式
- 效率更高,程序没有在两种不同的操作中来回切换
3、multiprocess.Process模块
1、Process模块介绍
- process模块是一个创建进程的模块,借助这个模块,就可以完成进程的创建
Process([group [, target [, name [, args [, kwargs]]]]]),由该类实例化得到的对象,表示一个子进程中的任务(尚未启动)
强调:
1. 需要使用关键字的方式来指定参数
2. args指定的为传给target函数的位置参数,是一个元组形式,必须有逗号
参数介绍:
1 group参数未使用,值始终为None
2 target表示调用对象,即子进程要执行的任务
3 args表示调用对象的位置参数元组,args=(1,2,'egon',)
4 kwargs表示调用对象的字典,kwargs={'name':'egon','age':18}
5 name为子进程的名称
- 方法介绍
1 p.start():启动进程,并调用该子进程中的p.run()
2 p.run():进程启动时运行的方法,正是它去调用target指定的函数,我们自定义类的类中一定要实现该方法
3 p.terminate():强制终止进程p,不会进行任何清理操作,如果p创建了子进程,该子进程就成了僵尸进程,使用该方法需要特别小心这种情况。如果p还保存了一个锁那么也将不会被释放,进而导致死锁
4 p.is_alive():如果p仍然运行,返回True
5 p.join([timeout]):主线程等待p终止(强调:是主线程处于等的状态,而p是处于运行的状态)。timeout是可选的超时时间,需要强调的是,p.join只能join住start开启的进程,而不能join住run开启的进程
- 属性介绍
1 p.daemon:默认值为False,如果设为True,代表p为后台运行的守护进程,当p的父进程终止时,p也随之终止,并且设定为True后,p不能创建自己的新进程,必须在p.start()之前设置
2 p.name:进程的名称
3 p.pid:进程的pid
4 p.exitcode:进程在运行时为None、如果为–N,表示被信号N结束(了解即可)
5 p.authkey:进程的身份验证键,默认是由os.urandom()随机生成的32字符的字符串。这个键的用途是为涉及网络连接的底层进程间通信提供安全性,这类连接只有在具有相同的身份验证键时才能成功(了解即可)
- 再windows中使用Process模块注意事项
在Windows操作系统中由于没有fork(linux操作系统中创建进程的机制),在创建子进程的时候会自动 import 启动它的这个文件,而在 import 的时候又执行了整个文件。因此如果将process()直接写在文件中就会无限递归创建子进程报错。所以必须把创建子进程的部分使用if __name__ ==‘__main__’ 判断保护起来,import 的时候 ,就不会递归运行了。
2、使用process模块创建进程
-
在一个python进程中开启子进程,start方法和并发效果
-
在python中启动的第一子进程
import time
from multiprocessing import Process
def f(name):
print('hello', name)
print('我是子进程')
if __name__ == '__main__':
p = Process(target=f, args=('bob',))
p.start()
time.sleep(1)
print('执行主进程的内容了')
- join方法
import time
from multiprocessing import Process
def f(name):
print('hello', name)
time.sleep(1)
print('我是子进程')
if __name__ == '__main__':
p = Process(target=f, args=('bob',))
p.start()
#p.join()
print('我是父进程')
- 查看子进程和主进程的进程号
import os
from multiprocessing import Process
def f(x):
print('子进程id :',os.getpid(),'父进程id :',os.getppid())
return x*x
if __name__ == '__main__':
print('主进程id :', os.getpid())
p_lst = []
for i in range(5):
p = Process(target=f, args=(i,))
p.start()
3、同时运行多个进程
import time
from multiprocessing import Process
def f(name):
print('hello', name)
time.sleep(1)
if __name__ == '__main__':
p_lst = []
for i in range(5):
p = Process(target=f, args=('bob',))
p.start()
p_lst.append(p)
import time
from multiprocessing import Process
def f(name):
print('hello', name)
time.sleep(1)
if __name__ == '__main__':
p_lst = []
for i in range(5):
p = Process(target=f, args=('bob',))
p.start()
p_lst.append(p)
p.join()
# [p.join() for p in p_lst]
print('父进程在执行')
import time
from multiprocessing import Process
def f(name):
print('hello', name)
time.sleep(1)
if __name__ == '__main__':
p_lst = []
for i in range(5):
p = Process(target=f, args=('bob',))
p.start()
p_lst.append(p)
# [p.join() for p in p_lst]
print('父进程在执行')
4、进程锁(互斥锁)----------multiprocess.Lock
通过刚刚的学习,我们千方百计实现了程序的异步,让多个任务可以同时在几个进程中并发处理,他们之间的运行没有顺序,一旦开启也不受我们控制。尽管并发编程让我们能更加充分的利用IO资源,但是也给我们带来了新的问题。
当多个进程使用同一份数据资源的时候,就会引发数据安全或顺序混乱问题。
多进程抢占资源
import os
import time
import random
from multiprocessing import Process
def work(n):
print('%s: %s is running' %(n,os.getpid()))
time.sleep(random.random())
print('%s:%s is done' %(n,os.getpid()))
if __name__ == '__main__':
for i in range(3):
p=Process(target=work,args=(i,))
p.start()
使用锁来维护执行顺序
# 由并发变成了串行,牺牲了运行效率,但避免了竞争
import os
import time
import random
from multiprocessing import Process,Lock
def work(lock,n):
lock.acquire()
print('%s: %s is running' % (n, os.getpid()))
time.sleep(random.random())
print('%s: %s is done' % (n, os.getpid()))
lock.release()
if __name__ == '__main__':
lock=Lock()
for i in range(3):
p=Process(target=work,args=(lock,i))
p.start()
加锁可以保证多个进程修改同一块数据时,同一时间只能有一个任务可以进行修改,即串行的修改,没错,速度是慢了,但牺牲了速度却保证了数据安全。
虽然可以用文件共享数据实现进程间通信,但问题是:
1.效率低(共享数据基于文件,而文件是硬盘上的数据)
2.需要自己加锁处理
因此我们最好找寻一种解决方案能够兼顾:1、效率高(多个进程共享一块内存的数据)2、帮我们处理好锁问题。这就是mutiprocessing模块为我们提供的基于消息的IPC通信机制:队列和管道。
队列和管道都是将数据存放于内存中
队列又是基于(管道+锁)实现的,可以让我们从复杂的锁问题中解脱出来,
我们应该尽量避免使用共享数据,尽可能使用消息传递和队列,避免处理复杂的同步和锁问题,而且在进程数目增多时,往往可以获得更好的可获展性。
5、习题”超简易版火车票抢票“(基于高并发的tcp服务端和进程锁)
-
未优化前
-
ticket.txt
10 # 剩余票数
- check_write.py
# 抢票读写函数
def check_ticket():
"""
查看剩余票数
从文件中读取据
:return:
"""
with open('ticket.txt','r',encoding='utf-8') as f:
return f.readlines()
def write_ticket(count):
"""
进行抢票
将修改后的数据写入文件
:return:
"""
with open('ticket.txt', 'w', encoding='utf-8') as f:
f.write(count)
- server.py
# 抢票系统服务端
import socket
import check_write
from multiprocessing import Process, Lock
def servers(lock,sock,client_addr):
print('正在接收用户数据>>:')
# print(sock,client_addr)
print('接到来自{}用户的请求'.format(client_addr))
while True:
data = sock.recv(1024)
if len(data) == 0:
continue
res = check_write.check_ticket()
print('来自%s的用户购买车票%s张' % (client_addr, data.decode('utf-8')))
if int(data) > int(res[0]):
message = '车票剩余不足'
sock.send(message.encode('utf-8'))
print('已经向%s的用户回复:%s' % (client_addr, message))
break
lock.acquire() # 上锁
db = str(int(res[0]) - int(data))
check_write.write_ticket(db)
msg = '购买成功'
sock.send(msg.encode('utf-8'))
print('已经向%s的用户回复:%s' % (client_addr, msg))
lock.release() # 解锁
sock.close()
if __name__ == '__main__':
server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
# server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('127.0.0.1', 8010))
server.listen(5)
lock = Lock()
while True:
sock, client_addr = server.accept()
for i in range(5):
p = Process(target=servers, args=(lock,sock, client_addr))
p.start()
server.colse()
- client.py
# 抢票系统 客户端
import socket
import check_write
def see_ticket():
"""
查看车票
:return:
"""
res = check_write.check_ticket()
print('车票剩余个数为%s'%res[0])
def buy_ticket():
"""
购买车票
:return:
"""
client = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)
client.connect(('127.0.0.1',8010))
while True:
msg = input('请输入购买车票个数:').strip()
if not msg:continue
if msg == 'q':
print('退出')
break
client.send(msg.encode('utf-8'))
data = client.recv(1024)
print('来自服务器的消息:%s'%(data.decode('utf-8')))
if data.decode('utf-8') == '车票剩余不足':
break
client.close()
dic = {
'1':see_ticket,
'2':buy_ticket
}
def choose():
while True:
print("""
========= 选择功能 =========
1, 查看车票
2, 购买车票
=========== end ===========
""")
choose_input = input('请选择功能:').strip()
if choose_input == 'q':
print('退出')
break
if choose_input in dic.keys():
dic[choose_input]()
choose()
- 优化后
多进程同时抢票
#文件db的内容为:{"count":1}
#注意一定要用双引号,不然json无法识别
#并发运行,效率高,但竞争写同一文件,数据写入错乱
from multiprocessing import Process,Lock
import time,json,random
def search():
dic=json.load(open('db'))
print('\033[43m剩余票数%s\033[0m' %dic['count'])
def get():
dic=json.load(open('db'))
time.sleep(0.1) #模拟读数据的网络延迟
if dic['count'] >0:
dic['count']-=1
time.sleep(0.2) #模拟写数据的网络延迟
json.dump(dic,open('db','w'))
print('\033[43m购票成功\033[0m')
def task():
search()
get()
if __name__ == '__main__':
for i in range(100): #模拟并发100个客户端抢票
p=Process(target=task)
p.start()
加上Lock锁来保护数据安全
#文件db的内容为:{"count":5}
#注意一定要用双引号,不然json无法识别
#并发运行,效率高,但竞争写同一文件,数据写入错乱
from multiprocessing import Process,Lock
import time,json,random
def search():
dic=json.load(open('db'))
print('\033[43m剩余票数%s\033[0m' %dic['count'])
def get():
dic=json.load(open('db'))
time.sleep(random.random()) #模拟读数据的网络延迟
if dic['count'] >0:
dic['count']-=1
time.sleep(random.random()) #模拟写数据的网络延迟
json.dump(dic,open('db','w'))
print('\033[32m购票成功\033[0m')
else:
print('\033[31m购票失败\033[0m')
def task(lock):
search()
lock.acquire()
get()
lock.release()
if __name__ == '__main__':
lock = Lock()
for i in range(100): #模拟并发100个客户端抢票
p=Process(target=task,args=(lock,))
p.start()
三、进程间的通信 -------队列(multiprocess.Queue)
1、队列介绍
创建共享的进程队列,Queue是多进程安全的队列,可以使用Queue实现多进程之间的数据传递。
1、参数介绍
Queue([maxsize])
创建共享的进程队列。
参数 :maxsize是队列中允许的最大项数。如果省略此参数,则无大小限制。
底层队列使用管道和锁定实现。
2、方法介绍
Queue([maxsize])
创建共享的进程队列。maxsize是队列中允许的最大项数。如果省略此参数,则无大小限制。底层队列使用管道和锁定实现。另外,还需要运行支持线程以便队列中的数据传输到底层管道中。
Queue的实例q具有以下方法:
q.get( [ block [ ,timeout ] ] )
返回q中的一个项目。如果q为空,此方法将阻塞,直到队列中有项目可用为止。block用于控制阻塞行为,默认为True. 如果设置为False,将引发Queue.Empty异常(定义在Queue模块中)。timeout是可选超时时间,用在阻塞模式中。如果在制定的时间间隔内没有项目变为可用,将引发Queue.Empty异常。
q.get_nowait( )
同q.get(False)方法。
q.put(item [, block [,timeout ] ] )
将item放入队列。如果队列已满,此方法将阻塞至有空间可用为止。block控制阻塞行为,默认为True。如果设置为False,将引发Queue.Empty异常(定义在Queue库模块中)。timeout指定在阻塞模式中等待可用空间的时间长短。超时后将引发Queue.Full异常。
q.qsize()
返回队列中目前项目的正确数量。此函数的结果并不可靠,因为在返回结果和在稍后程序中使用结果之间,队列中可能添加或删除了项目。在某些系统上,此方法可能引发NotImplementedError异常。
q.empty()
如果调用此方法时 q为空,返回True。如果其他进程或线程正在往队列中添加项目,结果是不可靠的。也就是说,在返回和使用结果之间,队列中可能已经加入新的项目。
q.full()
如果q已满,返回为True. 由于线程的存在,结果也可能是不可靠的(参考q.empty()方法)。。
3、其他方法
q.close()
关闭队列,防止队列中加入更多数据。调用此方法时,后台线程将继续写入那些已入队列但尚未写入的数据,但将在此方法完成时马上关闭。如果q被垃圾收集,将自动调用此方法。关闭队列不会在队列使用者中生成任何类型的数据结束信号或异常。例如,如果某个使用者正被阻塞在get()操作上,关闭生产者中的队列不会导致get()方法返回错误。
q.cancel_join_thread()
不会再进程退出时自动连接后台线程。这可以防止join_thread()方法阻塞。
q.join_thread()
连接队列的后台线程。此方法用于在调用q.close()方法后,等待所有队列项被消耗。默认情况下,此方法由不是q的原始创建者的所有进程调用。调用q.cancel_join_thread()方法可以禁止这种行为。
4、代码示例
'''
multiprocessing模块支持进程间通信的两种主要形式:管道和队列
都是基于消息传递实现的,但是队列接口
'''
from multiprocessing import Queue
q=Queue(3)
#put ,get ,put_nowait,get_nowait,full,empty
q.put(3)
q.put(3)
q.put(3)
# q.put(3) # 如果队列已经满了,程序就会停在这里,等待数据被别人取走,再将数据放入队列。
# 如果队列中的数据一直不被取走,程序就会永远停在这里。
try:
q.put_nowait(3) # 可以使用put_nowait,如果队列满了不会阻塞,但是会因为队列满了而报错。
except: # 因此我们可以用一个try语句来处理这个错误。这样程序不会一直阻塞下去,但是会丢掉这个消息。
print('队列已经满了')
# 因此,我们再放入数据之前,可以先看一下队列的状态,如果已经满了,就不继续put了。
print(q.full()) #满了
print(q.get())
print(q.get())
print(q.get())
# print(q.get()) # 同put方法一样,如果队列已经空了,那么继续取就会出现阻塞。
try:
q.get_nowait(3) # 可以使用get_nowait,如果队列满了不会阻塞,但是会因为没取到值而报错。
except: # 因此我们可以用一个try语句来处理这个错误。这样程序不会一直阻塞下去。
print('队列已经空了')
print(q.empty()) #空了
5、子进程发送数据给父进程
import time
from multiprocessing import Process, Queue
def f(q):
q.put([time.asctime(), 'from Eva', 'hello']) #调用主函数中p进程传递过来的进程参数 put函数为向队列中添加一条数据。
if __name__ == '__main__':
q = Queue() #创建一个Queue对象
p = Process(target=f, args=(q,)) #创建一个进程
p.start()
print(q.get())
p.join()
6、批量生产数据放入队列再批量获取结果
import os
import time
import multiprocessing
# 向queue中输入数据的函数
def inputQ(queue):
info = str(os.getpid()) + '(put):' + str(time.asctime())
queue.put(info)
# 向queue中输出数据的函数
def outputQ(queue):
info = queue.get()
print ('%s%s\033[32m%s\033[0m'%(str(os.getpid()), '(get):',info))
# Main
if __name__ == '__main__':
multiprocessing.freeze_support()
record1 = [] # store input processes
record2 = [] # store output processes
queue = multiprocessing.Queue(3)
# 输入进程
for i in range(10):
process = multiprocessing.Process(target=inputQ,args=(queue,))
process.start()
record1.append(process)
# 输出进程
for i in range(10):
process = multiprocessing.Process(target=outputQ,args=(queue,))
process.start()
record2.append(process)
for p in record1:
p.join()
for p in record2:
p.join()
# 10896(get):10164(put):Wed Jul 21 16:12:33 2021
# 4692(get):12080(put):Wed Jul 21 16:12:33 2021
# 3992(get):13140(put):Wed Jul 21 16:12:33 2021
# 16272(get):12268(put):Wed Jul 21 16:12:33 2021
# 8296(get):16844(put):Wed Jul 21 16:12:33 2021
# 14596(get):12736(put):Wed Jul 21 16:12:33 2021
# 9148(get):11380(put):Wed Jul 21 16:12:34 2021
# 10044(get):15688(put):Wed Jul 21 16:12:34 2021
# 17352(get):12532(put):Wed Jul 21 16:12:34 2021
# 13788(get):16444(put):Wed Jul 21 16:12:34 2021
2、生产者和消费者模型
- 在并发编程中使用生产者和消费者模式能够解决绝大多数并发问题。该模式通过平衡生产线程和消费线程的工作能力来提高程序的整体处理数据的速度。
1、为什么要使用生产者和消费者模式
- 在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。
2、什么是生产者消费者模式
- 生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
3、基于队列实现生产者消费者模型
from multiprocessing import Process,Queue
import time,random,os
def consumer(q):
while True:
res=q.get()
time.sleep(random.randint(1,3))
print('\033[45m%s 吃 %s\033[0m' %(os.getpid(),res))
def producer(q):
for i in range(10):
time.sleep(random.randint(1,3))
res='包子%s' %i
q.put(res)
print('\033[44m%s 生产了 %s\033[0m' %(os.getpid(),res))
if __name__ == '__main__':
q=Queue()
#生产者们:即厨师们
p1=Process(target=producer,args=(q,))
#消费者们:即吃货们
c1=Process(target=consumer,args=(q,))
#开始
p1.start()
c1.start()
print('主进程')
# 主
# 7652 生产了 包子0
# 16460 吃 包子0
# 7652 生产了 包子1
# 16460 吃 包子1
# 7652 生产了 包子2
# 16460 吃 包子2
# 7652 生产了 包子3
# 16460 吃 包子3
# 7652 生产了 包子4
# 16460 吃 包子4
# 7652 生产了 包子5
# 7652 生产了 包子6
# 16460 吃 包子5
# 16460 吃 包子6
# 7652 生产了 包子7
# 16460 吃 包子7
# 7652 生产了 包子8
# 7652 生产了 包子9
# 16460 吃 包子8
# 16460 吃 包子9
此时的问题是主进程永远不会结束,原因是:生产者p在生产完后就结束了,但是消费者c在取空了q之后,则一直处于死循环中且卡在q.get()这一步。
解决方式无非是让生产者在生产完毕后,往队列中再发一个结束信号,这样消费者在接收到结束信号后就可以break出死循环。
from multiprocessing import Process,Queue
import time,random,os
def consumer(q):
while True:
res=q.get()
if res == None:break
time.sleep(random.randint(1,3))
print('\033[45m%s 吃 %s\033[0m' %(os.getpid(),res))
def producer(q):
for i in range(10):
time.sleep(random.randint(1,3))
res='包子%s' %i
q.put(res)
print('\033[44m%s 生产了 %s\033[0m' %(os.getpid(),res))
q.put(None)
if __name__ == '__main__':
q=Queue()
#生产者们:即厨师们
p1=Process(target=producer,args=(q,))
#消费者们:即吃货们
c1=Process(target=consumer,args=(q,))
#开始
p1.start()
c1.start()
print('主进程')
注意:结束信号None,不一定要由生产者发,主进程里同样可以发,但主进程需要等生产者结束后才应该发送该信号
from multiprocessing import Process,Queue
import time,random,os
def consumer(q):
while True:
res=q.get()
if res is None:break #收到结束信号则结束
time.sleep(random.randint(1,3))
print('\033[45m%s 吃 %s\033[0m' %(os.getpid(),res))
def producer(q):
for i in range(2):
time.sleep(random.randint(1,3))
res='包子%s' %i
q.put(res)
print('\033[44m%s 生产了 %s\033[0m' %(os.getpid(),res))
if __name__ == '__main__':
q=Queue()
#生产者们:即厨师们
p1=Process(target=producer,args=(q,))
#消费者们:即吃货们
c1=Process(target=consumer,args=(q,))
#开始
p1.start()
c1.start()
p1.join()
q.put(None) #发送结束信号
print('主')
但上述解决方法中, 有多个生产者时和多个消费者时,我们则需要一种很low的方法来解决
from multiprocessing import Process,Queue
import time,random,os
def consumer(q):
while True:
res=q.get()
if res is None:break #收到结束信号则结束
time.sleep(random.randint(1,3))
print('%s 吃 %s' %(os.getpid(),res))
def producer(name,q):
for i in range(2):
time.sleep(random.randint(1,3))
res='%s%s' %(name,i)
q.put(res)
print('%s 生产了 %s' %(os.getpid(),res))
if __name__ == '__main__':
q=Queue()
#生产者们:即厨师们
p1=Process(target=producer,args=('包子',q))
p2=Process(target=producer,args=('骨头',q))
p3=Process(target=producer,args=('泔水',q))
#消费者们:即吃货们
c1=Process(target=consumer,args=(q,))
c2=Process(target=consumer,args=(q,))
#开始
p1.start()
p2.start()
p3.start()
c1.start()
p1.join() #必须保证生产者全部生产完毕,才应该发送结束信号
p2.join()
p3.join()
q.put(None) #有几个消费者就应该发送几次结束信号None
q.put(None) #发送结束信号
print('主进程')
四、线程
1、什么是线程(threads)
线程---------->能够独立运行的基本单位
- 进程是计算机资源分配的最小单位,线程是cpu调度的最小单位。每个进程至少有一个线程
2、进程与线程的区别
进程与线程的区别可以分为以下四点
(1) 地址空间和其他资源(如打开文件):进程间相互独立,统一进程各线程之间数据共享,某进程内的线程在其他进程内不可见
(2)通信:[进程间通信][IPC],线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
(3)调度和切换:线程上下文切换比进程上下文切换要快得多。
(4)在多线程操作系统中,进程不是一个可执行的实体。
3、线程的特点
在多线程的操作系统中,通常是在一个进程中包括多个线程,每个线程都是作为利用CPU的基本单位,是花费最小开销的实体。线程具有以下属性。
(1)轻型实体
线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源。
线程的实体包括程序、数据和TCB。线程是动态概念,它的动态特性由线程控制块TCB(Thread Control Block)描述。
TCP包括以下信息
TCB包括以下信息:
(1)线程状态。
(2)当线程不运行时,被保存的现场资源。
(3)一组执行堆栈。
(4)存放每个线程的局部变量主存区。
(5)访问同一个进程中的主存和其它资源。
用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。
(2)独立调度和分派的基本单位。
在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的)。
(3)共享进程资源。
线程在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的进程id,这意味着,线程可以访问该进程的每一个内存资源;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核。
(4)可并发执行
在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行,充分利用和发挥了处理机与外围设备并行工作的能力。
4、如何创建线程
线程的创建使用的Threading.Thread类
创建线程的方式1
from threading import Thread
import time
def task(name):
time.sleep(2)
print('%s真帅'%name)
if __name__ == '__main__':
t = Thread(target=task,args=('zb',) )
t.start
print('主线程')
# 主线程
# zb真帅
创建线程的方式2
from threading import Thread
import time
class func(Thread):
def __init__(self,name):
super().__init__()
self.name = name
def run(self):
time.sleep(2)
print('%s太帅了,已经遭不住了'%self.name)
if __name__ == '__main__':
t = func('zb')
t.start()
print('主进程')
# 主进程
# zb太帅了,已经遭不住了
5、多线程与多进程的比较
pid的比较
import time
from threading import Thread
from multiprocessing import Process
import os
def task():
print('hello world',os.getpid())
if __name__ == '__main__':
t = Thread(target=task)
t2 = Thread(target=task)
t.start()
t2.start()
print('线程的pid为:%s'%os.getpid())
p = Process(target=task)
p2 = Process(target=task)
p.start()
time.sleep(3)
p2.start()
print('进程的pid为:%s'%os.getpid())
# hello world 12692
# hello world 12692
# 线程的pid为:12692
# hello world 13820
# 进程的pid为:12692
# hello world 13804
效率的比较
from threading import Thread
from multiprocessing import Process
def task():
print('hello world')
if __name__ == '__main__':
t = Thread(target=task)
t.start()
print('主线程/主进程')
"""
hello world
主线程/主进程
"""
p = Process(target=task)
p.start()
print('主线程/主进程')
"""
主线程/主进程
hello world
"""
6、内存数据共享问题
from threading import Thread
from multiprocessing import Process
def work():
global n
n=0
print('子', n)
if __name__ == '__main__':
n=100
p=Process(target=work)
p.start()
p.join()
print('主',n) #毫无疑问子进程p已经将自己的全局的n改成了0,但改的仅仅是它自己的,查看父进程的n仍然为100
n=1
t=Thread(target=work)
t.start()
t.join()
print('主',n) #查看结果为0,因为同一进程内的线程之间共享进程内的数据
# 子 0
# 主 100
# 子 0
# 主 0
7、Thread类的其他方法
Thread实例对象的方法
# isAlive(): 返回线程是否活动的。
# getName(): 返回线程名。
# setName(): 设置线程名。
threading模块提供的一些方法:
# threading.currentThread(): 返回当前的线程变量。
# threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
# threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
代码示例
from threading import Thread
import threading
from multiprocessing import Process
import os
def work():
import time
time.sleep(3)
print(threading.current_thread().getName())
if __name__ == '__main__':
#在主进程下开启线程
t=Thread(target=work)
t.start()
print(threading.current_thread().getName()) # 返回当前线程的名字
print(threading.current_thread()) # 返回主线程变量名
print(threading.enumerate()) # 连同主线程在内有两个运行的线程的列表
print(threading.active_count()) # 返回主线程内运行线程的个数
print('主线程/主进程')
守护线程
**无论是进程还是线程,都遵循:守护xx会等待主xx运行完毕后被销毁。**需要强调的是:运行完毕并非终止运行
#1.对主进程来说,运行完毕指的是主进程代码运行完毕
#2.对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕
代码实例
from threading import Thread
import time
def sayhi(name):
time.sleep(2)
print('%s say hello' %name)
if __name__ == '__main__':
t=Thread(target=sayhi,args=('egon',))
t.setDaemon(True) #必须在t.start()之前设置
t.start()
print('主线程')
print(t.is_alive())
线程互斥锁
- 保护数据(只要牵扯到数据的改变,必须使用锁)
import time
from threading import Thread ,Lock
mutex = Lock()
money = 10
def task():
global money
mutex.acquire()
tmp = money
time.sleep(0.1)
money = tmp -1
mutex.release()
if __name__ == '__main__':
t_list = []
for i in range(10):
t = Thread(target=task)
t.start()
t_list.append(t)
for i in t_list:
i.join()
print(money)
# 0
五、全局解释器锁(GIL)
1、什么是GIL
- 即全局解释器锁(global interpreter lock),每个线程在执行时候都需要先获取GIL,保证同一时刻只有一个线程可以执行代码,即同一时刻只有一个线程使用CPU,也就是说多线程并不是真正意义上的同时执行。
2、它的作用是什么
3、GIL产生的原因
- 创建python时就只考虑到单核cpu。
- 解决多线程之间数据完整性和状态同步的最简单方法自然就是加锁。
六、线程
1、什么是协程
# 知识储备:
# 进程: 资源单位. 多进程下实现并发. 如果多核就是出现并行
# 线程: 执行单位. 同一进程下的多线程实现并发.
# 协程: (提示: 这个概念完全是程序员自己意淫出来的根本不存在)
# 协程就是在单线程下实现并发
2、为什么要用协程?
# 知识储备: 多道技术.
多道计数的核心就是切换+保存状态
切换分2种情况:
1) 程序在运行的过程中遇到了IO
2) 程序的执行时间过长或者有一个优先级更高的程序替代了它
# 为什么要用协程?
多道技术可以控制内核级别程序遇到IO或执行时间过长的情况下保存状态以后剥夺程序的CPU执行权限, 进而提升程序的执行效率.
我们可以在单线程内开启协程, 控制应用程序代码级别遇到IO情况下保存状态以后切换, 以此来提升效率. (提示: 如果是非IO操作的切换与效率无关)
# 协程的优点: 应用程序级别速度要远远高于操作系统的切换
# 协程的缺点: 多个任务一旦有一个阻塞没有切,整个线程都阻塞在原地, 该线程内的其他的任务都不能执行了.
# 强调!!!:
1) 一旦引入协程,就需要检测单线程下所有的IO行为,
2) 实现遇到IO就切换,少一个都不行,以为一旦一个任务阻塞了,整个线程就阻塞了,
3) 其他的任务即便是可以计算,但是也无法运行了
验证: 切换是否就一定提升效率
我们可以基于yield来验证。yield本身就是一种在单线程下可以保存任务运行状态的方法.
# 知识回顾
'''
1. yiled可以保存状态,yield的状态保存与操作系统的保存线程状态很像,但是yield是代码级别控制的,更轻量级
2. send可以把一个函数的结果传给另外一个函数,以此实现单线程内程序之间的切换
'''
# 串行执行计算密集型的任务
import time
def func1():
for i in range(10000000):
i + 1
def func2():
for i in range(10000000):
i + 1
start_time = time.time()
func1()
func2()
print(time.time() - start_time) # 执行时间: 1.1209993362426758
# 切换 + yield
import time
def func1():
while True:
10000000 + 1
yield
def func2():
g = func1() # 先初始化出生成器
for i in range(10000000):
i + 1
next(g)
start_time = time.time()
func2()
print(time.time() - start_time) # 执行时间: 1.4919734001159668
# 总结由此而知: 如果是非IO操作的切换与效率无关
'''
yield缺陷: yield不能检测IO,实现遇到IO自动切换. 接下来我们使用第三方gevent模块实现
'''
3、使用第三方gevent模块实现单线程下的协程
1 安装
# 前提: 安装了环境变量. 这里使用的是清华的源地址, 默认国外的地址下载速度太慢了!!
pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple gevent
2.、用法
Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程, 在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。
g1 = gevent.spawn(func, 1,, 2, 3, x = 4, y = 5)创建一个协程对象g1,spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数eat的. spawn内部调用了g.start()是一种类始于开启进程的异步提交任务的操作.
g2 = gevent.spawn(func2)
g1.join() # 等待g1结束
g2.join() # 等待g2结束
# 或者上述两步合作一步:gevent.joinall([g1,g2])
g1.value # 拿到func1的返回值
3、协程实现
'''
# spawn /spɔːn/ 再生侠 闪灵悍将 繁衍
# patch /pætʃ/ 补丁 修补 修补文件
注意!!!: from gevent import monkey;monkey.patch_all()必须放到被打补丁者的前面,如time,socket模块之前. 因为gevent在没有打补丁的情况下只能识别gevent自带的IO操作.
'''
from gevent import monkey;monkey.patch_all()
import time
import gevent
"""
gevent模块本身无法检测常见的一些io操作
在使用的时候需要你额外的导入一句话
from gevent import monkey
monkey.patch_all()
又由于上面的两句话在使用gevent模块的时候是肯定要导入的
所以还支持简写
from gevent import monkey;monkey.patch_all()
"""
def heng():
print('哼')
time.sleep(2)
print('哼1')
def ha():
print('哈')
time.sleep(3)
print('哈1')
def heiheihei():
print('heiheihei')
time.sleep(5)
print('heiheihei1')
# 情况1: 单线程默认执行情况下耗时统计
'''
start_time = time.time()
heng()
ha()
heiheihei()
print(time.time() - start_time) # 10.006284236907959
'''
# 情况2: 单线程使用gevent实现协程遇到IO切换+保存状态耗时统计
# 第一种写法:
'''
start_time = time.time()
g1 = gevent.spawn(heng) # 内部使用了g.start()
g2 = gevent.spawn(ha)
g3 = gevent.spawn(heiheihei)
g1.join()
g2.join() # 等待被检测的任务执行完毕 再往后继续执行
g3.join()
print(time.time() - start_time) # 5.006734848022461
'''
# 第二种写法: 如果是多个spawn就不要直接在后面使用join了, 不然会变成串行执行.而是使用第一种和第二种方式.
'''
start_time = time.time()
gevent.spawn(heng).join() # 内部使用了g.start()
gevent.spawn(ha).join()
gevent.spawn(heiheihei).join()
print(time.time() - start_time) # 10.006813526153564
'''
# 第三种写法: 是基于第一种写法的简写
start_time = time.time()
g1 = gevent.spawn(heng) # 内部使用了g.start()
g2 = gevent.spawn(ha)
g3 = gevent.spawn(heiheihei)
gevent.joinall([g1, g2, g3])
print(time.time() - start_time) # 5.006734848022461
4、协程应用: 使用gevent模块实现单线程下的socket并发
通过gevent实现单线程下的socket并发(from gevent import monkey;monkey.patch_all()一定要放到导入socket模块之前,否则gevent无法识别socket的阻塞
1、TCP服务端
from gevent import monkey; monkey.patch_all()
import gevent
from socket import *
'''
如果不想用money.patch_all()打补丁,可以用gevent自带的socket
from gevent import socket
s=socket.socket()
'''
def communication(conn):
while True:
try:
data_bytes = conn.recv(1024)
if not data_bytes:
break
conn.send(data_bytes.upper())
except ConnectionResetError as e:
print(e)
break
conn.close()
def server_forever(ip, port): # forever /fərˈevə(r)/ 永远 直到永远 永恒
server = socket(AF_INET, SOCK_STREAM)
server.bind((ip, port))
server.listen(5)
while True:
conn, client_address = server.accept()
# 检测communication中的recv的或者send的IO行为.(主要检测accept)
gevent.spawn(communication, conn)
if __name__ == '__main__':
'''
网络号为127的地址保留用于环回测试本机的进程间通信(127.0.0.0到127.255.255.255是保留地址,用于环回测试,0.0.0.0到0.255.255.255也是保留地址,用于表示所有的IP地址。
'''
# 检测server_forever中的accept的IO行为.
g = gevent.spawn(server_forever, '127.0.0.2', 8080)
g.join() # 等待g运行结束. 也就是说一直运行server_forever中的True循环中的代码. 如果这里不指定上面的spawn是异步提交的任务, 整个程序会直接结束.
2、TCP客户端
# 多线程并发多个客户端
from threading import Thread
from threading import current_thread
from socket import *
def client_communication(ip, port):
client = socket(AF_INET, SOCK_STREAM)
client.connect((ip, port))
count = 0
while True:
client.send(f'{current_thread().name} say hello!'
f' {count}'.encode('utf-8'))
count += 1
data_bytes = client.recv(1024)
print(data_bytes)
client.close()
if __name__ == '__main__':
for i in range(100):
t = Thread(target=client_communication, args=('127.0.0.2', 8080))
t.start()
5、总结
理想状态, 我们可以通过:
多进程下面开设多线程
多线程下面再开设协程序
从而使我们的程序执行效率提升