[并发编程 - socketserver模块实现并发、[进程查看父子进程pid、僵尸进程、孤儿进程、守护进程、互斥锁、队列、生产者消费者模型]
socketserver模块实现并发
基于tcp的套接字,关键就是两个循环,一个链接循环,一个通信循环
socketserver模块中分两大类:server类(解决链接问题)和request类(解决通信问题)
socketserver模块的使用
基于TCP协议的套接字--支持并发(拿之前通信的例子,主要针对服务端,客户端不变)
# 服务端.py
import socketserver # 导入模块
# 先定义一个类,这个类专门解决通信循环的,必须继承一个类BaseRequestHandler
class MyRequestHandler(socketserver.BaseRequestHandler):
def handle(self): # 必须要写一个函数,叫handle的方法,里面放通信循环
while True:
try:
data = self.request.recv(1024) # 最大接收的字节数
if len(data) == 0:
break
print(data)
self.request.send(data.upper())
except Exception:
break
self.request.close()
# 链接循环,套接字属于IO密集型,对于IO密集型应该使用多线程
# 多线程ThreadingTCPServer里面放:监听的服务端ip和端口、定义的类、bind_and_activate=True
# bind_and_activate=True等同于bind()并且listen()默认属性为True无需添加。
server = socketserver.ThreadingTCPServer(('127.0.0.1',8080),MyRequestHandler,bind_and_activate=True)
# 一直对外提供服务
server.serve_forever()
# serve_forever()每建成一个链接,都调用MyRequestHandler这个类,创建一个对象,
# 把它建成的链接对象赋值给self下面的request进行通信
# 整体逻辑:相当于客户端每发来一个请求,服务端就启一个线程,每启一个线程就去运行对象下面的
# handle方法,把跟这个客户端所有相关的套接字信息全都放到self对象里面去并触发这个对象下面
# 的handle方法用这个方法跟客户端进行通信
# 客户端.py (可实现多个客户端同时通信)
import socket
# 1、买手机
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 流式协议
# 2、打电话
phone.connect(('127.0.0.1',8080))
# 3、发\收数据
while True:
msg = input('>>>: ').strip()
if len(msg) == 0:
continue
phone.send(msg.encode('utf-8'))
data = phone.recv(1024)
print(data.decode('utf-8'))
# 4、关闭
phone.close()
基于UDP协议的套接字--支持并发
# 服务端.py
import socketserver
class MyRequesthanlder(socketserver.BaseRequestHandler):
# 必须要写一个函数,叫handle的方法,里面放通信循环
def handle(self):
# 收到消息,进行解压。第一个值是客户端发来的数据。第二个值是套接字对象,用它来回消息
data,server = self.request
# 将收到的消息转大写回复,所有套接字信息都封装进self里了
server.sendto(data.upper(),self.client_address)
server = socketserver.ThreadingUDPServer(('127.0.0.1',9999),MyRequesthanlder)
server.serve_forever()
# 整体逻辑同上面TCP协议一样
# 客户端.py
from socket import *
client = socket(AF_INET,SOCK_DGRAM)
while True:
msg = input(">>>>:").strip()
client.sendto(msg.encode('utf-8'),('127.0.0.1',9999))
res,server_addr = client.recvfrom(1024)
print(res.decode('utf-8'))
补充知识点:关于查看父进程子进程pid
from multiprocessing import Process
import time
import os
def task(name,age):
# 如果在任务中取出进程id号,需要使用os模块
print('当前进程(子进程)id号是:',os.getpid()) #当前进程id号
print('当前进程父进程的id号是:',os.getppid()) # 当前进程父进程的id号
time.sleep(10)
print(name)
print(age)
if __name__ == '__main__':
p=Process(target=task,kwargs={'age':19,'name':'lqz'})
p.start()
print('p这个进程的id号是:',p.pid) # 进程id号
print('当前进程id(主进程)号是:', os.getpid()) # 当前进程id号
print('当前进程父进程(pycharm)的id号是:', os.getppid()) # 当前进程父进程的id号
time.sleep(10)
'''
如果有p对象,就是用p.pid获取进程id号
如果没有p对象,就是用os模块的
os.getpid() #当前进程id号
os.getppid() #父进程id号
僵尸进程
一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。详解如下
我们知道在正常情况下子进程是通过父进程创建的,子进程在创建新的进程。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程到底什么时候结束,子进程结束,父进程没有明确的答复操作系统内核:已收到子进程结束的消息。此时操作系统内核会一直保存该子进程的部分PCB信息,即为僵尸进程
僵尸进程的危害:占用PCB资源(主要是PID资源),系统将会因为产生大量的僵尸进程,而没有可用的进程号来产生新进程,导致系统资源不足
因此,UNⅨ提供了一种机制可以保证父进程可以在任意时刻获取子进程结束时的状态信息:
1、在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)
2、直到父进程通过wait / waitpid来取时才释放. 但这样就导致了问题,如果进程不调用wait / waitpid的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。
任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个子进程在结束时都要经过的阶段。如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。如果父进程能及时 处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。 如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。
"""
僵尸进程危害场景:
例如有个进程,它定期的产 生一个子进程,这个子进程需要做的事情很少,做完它该做的事情之后就退出了,因此这个子进程的生命周期很短,但是,父进程只管生成新的子进程,至于子进程 退出之后的事情,则一概不闻不问,这样,系统运行上一段时间之后,系统中就会存在很多的僵死进程,倘若用ps命令查看的话,就会看到很多状态为Z的进程。 严格地来说,僵死进程并不是问题的根源,罪魁祸首是产生出大量僵死进程的那个父进程。因此,当我们寻求如何消灭系统中大量的僵死进程时,答案就是把产生大 量僵死进程的那个元凶枪毙掉(也就是通过kill发送SIGTERM或者SIGKILL信号啦)。枪毙了元凶进程之后,它产生的僵死进程就变成了孤儿进 程,这些孤儿进程会被init进程接管,init进程会wait()这些孤儿进程,释放它们占用的系统进程表中的资源,这样,这些已经僵死的孤儿进程 就能瞑目而去了。
"""
# 测试:
# 手动制造一个产生僵尸进程的程序test.py内容如下
# coding:utf-8
from multiprocessing import Process
import time,os
def run():
print('子',os.getpid())
if __name__ == '__main__':
p=Process(target=run)
p.start()
print('主',os.getpid())
time.sleep(1000)
清除僵尸进程的三种解决方法
核心思想:父进程的知道子进程的结束,并且明确的回复操作系统,此时操作系统才能回收资源,避免僵尸进程的产生
第一种方法就是结束父进程(一般是主进程)。当父进程退出的时候僵尸进程随后也会被清除。 当然这个是个暴力的手段,因为我们一般肯定是希望父进程继续运行的。
第二种方法就是通过
wait
调用来读取子进程退出状态。比如通过multiprocessing.Process
产出的进程可以通过子进程的join()
方法来 wait,也可以在父进程中处理SIGCHLD
信号,在处理程序中调用wait
系统调用或者直接设置为SIG_IGN
来清除僵尸进程。第三种办法就说把进程变成孤儿进程,这样进程就会自动交由 init 进程(pid 为 1 的进程)来处理,一般 init 进程都包含对僵尸进程进行处理的逻辑。(这里有个坑,那就是 docker 容器中一般 pid 为 1 进程就是主程序的进程,而不是我们预期的 init 进程。如果要使用这种方法的话,需要注意一下类似的场景)
孤儿进程
当父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程,由于进程不可能脱离进程树而独立存在,孤儿进程将被PID为1的init进程所收养,并由init进程对它们完成状态收集工作。孤儿进程被收养后进行正常的释放,没有危害
创建完子进程后,主进程所在的这个脚本就退出了,当父进程先于子进程结束时,子进程会被init收养,成为孤儿进程,而非僵尸进程。演示如下:
import os,sys
import time
pid = os.getpid()
ppid = os.getppid()
print 'im father', 'pid', pid, 'ppid', ppid
pid = os.fork()
# 执行pid=os.fork()则会生成一个子进程
# 返回值pid有两种值:
# 如果返回的pid值为0,表示在子进程当中
# 如果返回的pid值>0,表示在父进程当中
if pid > 0:
print 'father died..'
sys.exit(0)
# 保证主线程退出完毕
time.sleep(1)
print 'im child', os.getpid(), os.getppid()
# 执行文件,输出结果:
im father pid 32515 ppid 32015
father died..
im child 32516 1
# 看,子进程已经被pid为1的init进程接收了,所以僵尸进程在这种情况下是不存在的,
# 存在只有孤儿进程而已,孤儿进程声明周期结束自然会被init来销毁。
守护进程
守护一个服务,长期驻留在内存中提供服务,不能够受制于终端;
如何让一个进程成为守护进程?
主进程创建守护进程
其一:守护进程会在主进程代码执行结束后就终止
其二:守护进程内无法再开启子进程,否则抛出异常:AssertionError: daemonic processes are not allowed to have children
守护进程就是Daemon程序,也称为精灵进程,是一种在系统后台执行的程序,它独立于控制终端并且执行一些周期任务或触发事件,通常被命名为"d"字母结尾,如常见的httpd、syslogd、systemd和dockerd等
注意:进程之间是互相独立的,主进程代码运行结束,守护进程随即终止
from multiprocessing import Process
import os,time
def task():
print("进程%s开启" %os.getpid()) # 2.接着会打印进程开启,可以看到打印结果
time.sleep(10)
print("进程%s结束" %os.getpid()) # 看不到,在time.sleep(10)的时候主进程代码结束
if __name__ == '__main__':
p = Process(target=task)
p.daemon = True # 调用守护进程
p.start()
print("主:%s" %os.getpid()) # 1.先打印"主",主进程睡三秒的时间足够守护进程启动起来
time.sleep(3)
# 主进程代码运行完毕,守护进程就会结束
from multiprocessing import Process
import time
def foo():
print(123)
time.sleep(1)
print("end123")
def bar():
print(456)
time.sleep(3)
print("end456")
if __name__ == '__main__':
p1=Process(target=foo)
p2=Process(target=bar)
p1.daemon=True
p1.start()
p2.start()
print("main-------")
# 打印可能出现的三种结果:
"""
main-------
456
end456
"""
"""
main-------
123
456
end456
"""
"""
123
main-------
456
end456
"""
互斥锁(同步锁)
进程之间数据不共享,但是共享同一套文件系统,所以访问同一个文件,或同一个打印终端,是没有问题的,
而共享带来的是竞争,竞争带来的结果就是错乱,如何控制,就是加锁处理
part1:多个进程共享同一打印终端
# 并发运行,效率高,但竞争同一打印终端,带来了打印错乱
from multiprocessing import Process
import os,time
def work():
print("进程%s开启" %os.getpid())
time.sleep(2)
print("进程%s结束" %os.getpid())
if __name__ == '__main__':
for i in range(3):
p=Process(target=work)
p.start()
# 加锁:由并发变成了串行,牺牲了运行效率,但避免了竞争
from multiprocessing import Process,Lock
import os,time
def work(lock):
lock.acquire() # 锁定
print("进程%s开启" %os.getpid())
time.sleep(2) # 模拟网络延迟
print("进程%s结束" %os.getpid())
lock.release() # 释放
if __name__ == '__main__':
lock=Lock() # 创建锁
for i in range(3):
p=Process(target=work,args=(lock,))
p.start()
part2:多个进程共享同一文件
文件当数据库,模拟抢票
# 文件db的内容为:{"count":1}
# 注意一定要用双引号,不然json无法识别
from multiprocessing import Process,Lock
import json
import os
import time
def check():
with open('db.json',mode='rt',encoding='utf-8') as f:
time.sleep(1)
dic = json.load(f)
print("%s查看到剩余票数为:%s" %(os.getpid(),dic['count']))
def get():
with open('db.json',mode='rt',encoding='utf-8') as f:
time.sleep(1) # 模拟读数据的网络延迟
dic = json.load(f)
if dic['count'] > 0:
dic['count'] -= 1
time.sleep(3) # 模拟写数据的网络延迟
with open('db.json',mode='wt',encoding='utf-8') as f:
json.dump(dic,f)
print('%s购票成功' %os.getpid())
else:
print("购票失败")
def func(mutex):
check()
mutex.acquire()
get()
mutex.release()
# with mutex:
# get()
if __name__ == '__main__':
mutex = Lock() # 在父进程创建锁
for i in range(10): #模拟并发10个客户端抢票
p = Process(target=func,args=(mutex,))
p.start()
# p.join()
print("主")
加锁可以保证多个进程修改同一块数据时,同一时间只能有一个任务可以进行修改,即串行的修改,没错,速度是慢了,但牺牲了速度却保证了数据安全。
虽然可以用文件共享数据实现进程间通信,但问题是:
1.效率低(共享数据基于文件,而文件是硬盘上的数据)
2.需要自己加锁处理
队列
进程彼此之间互相隔离,要实现进程间通信(IPC),multiprocessing模块支持两种形式:队列和管道,这两种方式都是使用消息传递的
创建队列的类(底层就是以管道和锁定的方式实现):
Queue([maxsize]):创建共享的进程队列,Queue是多进程安全的队列,可以使用Queue实现多进程之间的数据传递。
参数介绍:
maxsize是队列中允许最大项数,省略则无大小限制。
方法介绍:
q.put方法用以插入数据到队列中,put方法还有两个可选参数:blocked和timeout。blocked为True(默认值)如果
队列满了就锁住了并且timeout为正值,该方法会阻塞timeout指定的时间,直到该队列有剩余的空间。如果超时,会
抛出Queue.Full异常。如果blocked为False,但该Queue已满,会立即抛出Queue.Full异常。
q.get方法可以从队列读取并且删除一个元素。同样,get方法有两个可选参数:blocked和timeout。如果blocked
为True(默认值),并且timeout为正值,那么在等待时间内没有取到任何元素,会抛出Queue.Empty异常。如果
blocked为False有两种情况存在,如果Queue有一个值可用,则立即返回该值,否则,如果队列为空,则立即抛出
Queue.Empty异常.
q.get_nowait():同q.get(False)
q.put_nowait():同q.put(False)
q.empty():调用此方法时q为空则返回True,该结果不可靠,比如在返回True的过程中,如果队列中又加入了项目。
q.full():调用此方法时q已满则返回True,该结果不可靠,比如在返回True的过程中,如果队列中的项目被取走。
q.qsize():返回队列中目前项目的正确数量,结果也不可靠,理由同q.empty()和q.full()一样
其它方法(了解)
q.cancel_join_thread():不会在进程退出时自动连接后台线程。可以防止join_thread()方法阻塞
q.close():关闭队列,防止队列中加入更多数据。调用此方法,后台线程将继续写入那些已经入队列但尚未写入的
数据,但将在此方法完成时马上关闭。如果q被垃圾收集,将调用此方法。关闭队列不会在队列使用者中
产生任何类型的数据结束信号或异常。例如,如果某个使用者正在被阻塞在get()操作上,关闭生产者中
的队列不会导致get()方法返回错误。
q.join_thread():连接队列的后台线程。此方法用于在调用q.close()方法之后,等待所有队列项被消耗。默认
情况下,此方法由不是q的原始创建者的所有进程调用。调用q.cancel_join_thread方法可
以禁 止这种行为
队列应用
'''
共享内存:
1.管道
tasklist | findstr xxx
ps aux | grep xxx
2.队列
multiprocessing模块支持进程间通信的两种主要形式:管道和队列
都是基于消息传递实现的,但是队列接口
'''
from multiprocessing import Queue
q = Queue(3) # 创建共享的进程队列,指定队列的长度为3,最多放三个值
q.put([1,2,3]) # 放入值到队列中
q.put({"a":1})
q.put("xxx")
# q.put(1000) # 超出值无法放入
print(q.get())
print(q.get())
print(q.get())
# print(q.get()) # 超值取不到q.get()默认为 q.get(block=True,timeout=None)
# print(q.get(block=True,timeout=3)) # 取不到三秒抛出异常
print(q.get(block=False)) # 取不到值立马抛异常
生产者消费者模型
在并发编程中使用生产者和消费者模式能够解决绝大多数并发问题。该模式通过平衡生产线程和消费线程的工作能力来提高程序的整体处理数据的速度。
为什么要使用生产者和消费者模式
在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。
什么是生产者消费者模式
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
生产者消费者模型:
该模型有两种角色,一种是生产者,另外一种是消费者
生产者负责产生数据,消费者负责取走数据进行处理
生产者与消费者通过队列通信
优点:解耦合,平衡了生产者的生产力与消费者的处理能力
基于队列实现生产者消费者模型
import random
import time
from multiprocessing import Process, Queue
def producer(q, name, food):
for i in range(3):
res = "%s%s" % (food, i)
time.sleep(random.randint(1, 3))
q.put(res)
print("%s 生产了 %s" % (name, res))
def consumer(q, name):
while True:
res = q.get()
if res is None:
break
time.sleep(random.randint(1, 3))
print("%s 吃了 %s" % (name, res))
if __name__ == '__main__':
q = Queue() # 创建队列
p1 = Process(target=producer, args=(q,"厨师1", "包子"))
p2 = Process(target=producer, args=(q,"厨师2", "烧麦"))
p3 = Process(target=producer, args=(q,"厨师3", "馒头"))
c1 = Process(target=consumer, args=(q, "lxx"))
c2 = Process(target=consumer, args=(q, "hxx"))
p1.start()
p2.start()
p3.start()
c1.start()
c2.start()
p1.join()
p2.join()
p3.join()
q.put(None)
q.put(None)
print("主")
基于队列实现生产者消费者模型加入守护进程
import random
import time
from multiprocessing import Process, JoinableQueue
def producer(q, name, food):
for i in range(3):
res = "%s%s" % (food, i)
time.sleep(random.randint(1, 3))
q.put(res)
print("%s 生产了 %s" % (name, res))
q.join()
def consumer(q, name):
while True:
res = q.get()
if res is None:
break
time.sleep(random.randint(1, 3))
print("%s 吃了 %s" % (name, res))
q.task_done()
if __name__ == '__main__':
q = JoinableQueue()
p1 = Process(target=producer, args=(q,"厨师1", "包子"))
p2 = Process(target=producer, args=(q,"厨师2", "烧麦"))
p3 = Process(target=producer, args=(q,"厨师3", "馒头"))
c1 = Process(target=consumer, args=(q, "lxx"))
c2 = Process(target=consumer, args=(q, "hxx"))
c1.daemon = True # 主进程结束顺便带走了守护进程
c2.daemon = True
p1.start()
p2.start()
p3.start()
c1.start()
c2.start()
p1.join()
p2.join()
p3.join() # p1、p2、p3都结束,代表队列一定被取空
print("主")