python-网络编程-02-进程教程
基础理论
#一 操作系统的作用:
1:隐藏丑陋复杂的硬件接口,提供良好的抽象接口
2:管理、调度进程,并且将多个进程对硬件的竞争变得有序
#二 多道技术:
1.产生背景:针对单核,实现并发
现在的主机一般是多核,那么每个核都会利用多道技术
有4个cpu,运行于cpu1的某个程序遇到io阻塞,会等到io结束再重新调度,会被调度到4个
cpu中的任意一个,具体由操作系统调度算法决定。
2.空间上的复用:如内存中同时有多道程序
3.时间上的复用:复用一个cpu的时间片
强调:遇到io切换,占用cpu时间过长也会切换,核心在于切之前将进程的状态保存下来,这样
才能保证下次切换回来时,能基于上次切走的位置继续运行
一、进程
# 什么是进程
进程是程序(软件,应用)的一个执行实例,每个运行中的程序,可以同时创建多个进程,但至少要有一个。每个进程都提供执行程序所需的所有资源,都有一个虚拟的地址空间、可执行的代码、操作系统的接口、安全的上下文(记录启动该进程的用户和权限等等)、唯一的进程ID、环境变量、优先级类、最小和最大的工作空间(内存空间)。**进程可以包含线程**,并且**每个进程必须有至少一个线程**。每个进程启动时都会最先产生一个线程,即主线程,然后主线程会再创建其他的子线程。
# 进程与程序的区别
程序仅仅只是一堆代码而已,而进程指的是程序的运行过程
# 并发、并行、串行
并发: 看起来是同时执行的程序,但实际只要一个cpu在不同的切换执行不同的进程
并行: 真正的多个任务同时执行
串行: 如串串 只有吃完第一个(执行完)然后才会继续吃(进行) 下一个
1.1、进程-状态
创建\终止
# 进程创建
但凡是硬件,都需要有操作系统去管理,只要有操作系统,就有进程的概念,就需要有创建进程的方式,一些操作系统只为一个应用程序设计
创建流程:
1、系统初始化 (查看进程linux中用ps命令,windows中用任务管理器,前台进程负责与用户交互,后台运行的进程与用户无关,运行在后台并且只在需要时才唤醒的进程,称为守护进程,如电子邮件、web页面、新闻、打印 )
2、一个进程在运行过程中开启了子进程(linux: fork, windows: createPorcess)
3、用户的交互式请求,而创建一个新进程
4、一个批处理作业的初始化
无论哪一种,新进程的创建都是由一个已经存在的进程执行了一个用于创建进程的系统调用而创建的:
1. 在UNIX中该系统调用是:fork,fork会创建一个与父进程一模一样的副本,二者有相同的存储映像、同样的环境字符串和同样的打开文件(在shell解释器进程中,执行一个命令就会创建一个子进程)
2. 在windows中该系统调用是:CreateProcess,CreateProcess既处理进程的创建,也负责把正确的程序装入新进程。
# 进程的终止
1. 正常退出(自愿,如用户点击交互式页面的叉号,或程序执行完毕调用发起系统调用正常退出,在linux中用exit,在windows中用ExitProcess)
2. 出错退出(自愿,python a.py中a.py不存在)
3. 严重错误(非自愿,执行非法指令,如引用不存在的内存,1/0等,可以捕捉异常,try...except...)
4. 被其他进程杀死(非自愿,如kill -9)
状态切换
运行:正在被cpu执行的状态
阻塞: 当程序执行io时会被阻塞,cpu同时也会切换到其它进程
就绪:当程序io执行完成,cpu会切换回来成为就绪态
中断状态数据
进程并发的实现在于,硬件中断一个正在运行的进程,把此时进程运行的所有状态保存下来,为此,操作系统维护一张表格,即进程表(process table),每个进程占用一个进程表项(这些表项也称为进程控制块)
该表存放了进程状态的重要信息:程序计数器、堆栈指针、内存分配状况、所有打开文件的状态、帐号和调度信息,以及其他在进程由运行态转为就绪态或阻塞态时,必须保存的信息,从而保证该进程在再次启动时,就像从未被中断过一样。
示例
开启子进程
Python中的multiprocess提供了Process类,实现进程相关的功能。但是它基于fork机制,因此不被windows平台支持。想要在windows中运行,必须使用`if __name__ == '__main__:`的方式,显然这只能用于调试和学习,不能用于实际环境。
# 开启方式一
import time
from multiprocessing import Process
def test(x):
print("{} 开始".format(x))
time.sleep(2)
print("{} 结束".format(x))
if __name__ == '__main__':
pro = Process(target=test, args=("子进程",))
pro.start() # 给操作系统发起一个创建子程序的信号
print("主进程开始")
# 执行结果, 程序会先运行主进程中的代码,然后在运行子进程
主进程开始
子进程 开始
子进程 结束
# 开启方式二
import time
from multiprocessing import Process
class Myprocess(Process):
def __init__(self, x):
super().__init__()
self.x = x
def run(self): # 与直接调用不同, 如果用继承需要重新定义run方法
print("{} 开始".format(self.x))
time.sleep(1)
print("{} 结束".format(self.x))
if __name__ == '__main__':
pro = Myprocess("子进程")
pro.start()
print("主进程")
参数说明
Process([group [, target [, name [, args [, kwargs]]]]]),由该类实例化得到的对象,表示一个子进程中的任务(尚未启动)
# 强调:
1. 需要使用关键字的方式来指定参数
2. args指定的为传给target函数的位置参数,是一个元组形式,必须有逗号
# 参数:
group 参数未使用,值始终为None
target 表示调用对象,即子进程要执行的任务
args 表示调用对象的位置参数元组,args=(1,2,'egon',)
kwargs 表示调用对象的字典,kwargs={'name':'egon','age':18}
name 为子进程的名称
# 方法:
p.start():启动进程,并调用该子进程中的p.run()
p.run():进程启动时运行的方法,正是它去调用target指定的函数,我们自定义类的类中一定要实现该方法
p.terminate():强制终止进程p,不会进行任何清理操作,如果p创建了子进程,该子进程就成了僵尸进程,使用该方法需要特别小心这种情况。如果p还保存了一个锁那么也将不会被释放,进而导致死锁
p.is_alive():如果p仍然运行,返回True
p.join([timeout]):主线程等待p终止(强调:是主线程处于等的状态,而p是处于运行的状态)。timeout是可选的超时时间,需要强调的是,p.join只能join住start开启的进程,而不能join住run开启的进程
# 属性:
p.daemon:默认值为False,如果设为True,代表p为后台运行的守护进程,当p的父进程终止时,p也随之终止,并且设定为True后,p不能创建自己的新进程,必须在p.start()之前设置
p.name:进程的名称
p.pid:进程的pid
p.exitcode:进程在运行时为None、如果为–N,表示被信号N结束(了解即可)
p.authkey:进程的身份验证键,默认是由os.urandom()随机生成的32字符的字符串。这个键的用途是为涉及网络连接的底层进程间通信提供安全性,这类连接只有在具有相同的身份验证键时才能成功(了解即可)
僵尸\孤儿进程
僵尸进程是一种独特的数据结构,子进程运行完之后其所打开的文件、内存空间都会被释放,但会保留子程序的ID号, 僵尸进程的存在是为了父进程无法预知子进程的状态,而父进程却想获取到子进程的id信息
1.2、守护进程
当父进程需要将一个任务并发出去执行,需要将该任务放到子进程中,当该子进程内的代码在父进程代码运行完毕后就没有存在的意义了,那就应该将子进程设置为守护进程,这样就会在父进程执行完毕之后直接退出。
# 主进程创建守护进程
1:守护进程会在主进程代码执行结束后就终止
2:守护进程内无法再开启子进程,否则抛出异常:AssertionError: daemonic processes are not allowed to have children
注意:进程之间是互相独立的,主进程代码运行结束,守护进程随即终止
示例
import time
from multiprocessing import Process
def fp():
print("123")
time.sleep(1)
print("fp 123 end")
def dp():
print("456")
time.sleep(2)
print("dp 456 end")
if __name__ == '__main__':
p1 = Process(target=fp)
p2 = Process(target=dp)
p1.daemon = True
p1.start()
p2.start()
print("main----------- ")
"""
main-----------
456
dp 456 end
"""
1.3、互斥锁
将部分代码(涉及到修改共享数据的代码)变成串行,将并发变成串行,牺牲效率提高数据安全
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
import os
import time
import json
from multiprocessing import Process, Lock
# 查看票
def ticket_list():
time.sleep(1) # 模拟网络延时
with open("file.txt", "rt", encoding="utf-8") as read_f:
dic = json.load(read_f)
print("{} 当前还有{}票".format(os.getpid(), dic["count"]))
# 购买
def ticket_buy():
# 先查看票,如有票-1, 无票提示
time.sleep(1) # 模拟查看延时
with open("file.txt", "rt", encoding="utf-8") as read_f:
dic = json.load(read_f)
if dic["count"] > 0:
dic["count"] -= 1
time.sleep(1) # 如果不添加延时,只会依赖计算能力,在一定范围内能直接处理
with open("file.txt", "wt", encoding="utf-8") as wirte_f:
json.dump(dic, wirte_f)
print("{} 购票成功".format(os.getpid()))
else:
print("{} 无余票,购票失败".format(os.getpid()))
def task(mutex):
ticket_list()
mutex.acquire() # 互斥锁不能连续的acquire(), 必须是release以后才能acquire
ticket_buy()
mutex.release()
if __name__ == '__main__':
mutex = Lock()
for i in range(10):
p = Process(target=task, args=(mutex,))
p.start()
1.4、IPC
# 进程间通信,不应该使用硬盘空间,而是使用内存空间通信, 因此我们最好找寻一种解决方案能够兼顾:1、效率高(多个进程共享一块内存的数据)2、帮我们处理好锁问题。这就是mutiprocessing模块为我们提供的基于消息的IPC通信机制:队列和管道。
# IPC 进程间通信, 两种实现方式, 队列和管道都是将数据存放于内存中
pipe: 管道
queue: 队列基于(管道+锁)实现的,可以让我们从复杂的锁问题中解脱出来,
我们应该尽量避免使用共享数据,尽可能使用消息传递和队列,避免处理复杂的同步和锁问题,而且在进程数目增多时,往往可以获得更好的可获展性。
queue
- 类: 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()一样
1、队列占用的是内存空间
2、不应该往队列中放大数据,应该只存放数据量较小的消息
q = Queue(3) # 设置队列大小为3
# 使用-示
q.put("world3") # 正常添加
print(q.get())
# block=True时 timeout才会生效
q.put("hello", block=True, timeout=3)
print(q.get(block=True, timeout=3)) # 取出超时,当队列中没有时,超时会出现_queue.Empty
# block=False, 队列满了直接抛出异常,不阻塞
二、生产者和消费者
利用多线程和队列可以实现生产者消费者模式。该模式通过平衡生产线程和消费线程的工作能力来提高程序整体处理数据的速度。
# 什么是生产者和消费者
在线程世界里,生产者就是生产数据(或者说发布任务)的线程,消费者就是消费数据(或者说处理任务)的线程。在任务执行过程中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者提供更多的任务,本质上,这是一种供需不平衡的表现。为了解决这个问题,我们创造了生产者和消费者模式。
生产者 = 发布任务线程, 消费者 = 处理任务线程
# 什么时侯用?
当程序中存在明显的两类任务,一类负责生产数据,一类负责处理数据,此时就应该考虑使用生产者消费者模型来提升 程序的效率
工作机制
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而是通过 阻塞队列(消息队列) 来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不直接找生产者要数据,而是从阻塞队列里取,阻塞队列 (消息队列) 就相当于一个缓冲区,平衡了生产者和消费者的处理能力,解耦了生产者和消费者。
生产者消费者模式的核心是‘阻塞队列’也称消息队列。在生产环境中有很多大名鼎鼎的分布式消息队列,例如RabbitMQ,RocketMq,Kafka等等。在学习过程中,我们没必要使用这么大型的队列,直接使用Python内置的queue模块中提供的队列就可以了。
多进程-demo-1
q.task_done() # 消费者告知生产者取了一次
q.join() # 等待队列被取干净, 结束意味着主进程的代码运行完毕 --> 生产运行完毕并且队列中的数据也被取干净了--> 消费者也就没有意义了
""" # 正常写法,但这样的每添加一个进程都需要put一个None
import os
import random
import time
from multiprocessing import Process, Queue
def product(q):
for i in range(2):
time.sleep(random.randint(1, 3))
print("\033[45m{} 生产了 包子 {}\033[0m".format(os.getpid(), i))
q.put(i)
# q.put(None)
def consume(q):
while True:
time.sleep(random.randint(1, 3))
res = q.get()
if res is None: break
print("{} 吃掉了 包子 {}".format(os.getpid(), res))
if __name__ == '__main__':
q = Queue()
p1 = Process(target=product, args=(q,))
p2 = Process(target=product, args=(q,))
p3 = Process(target=product, args=(q,))
c1 = Process(target=consume, args=(q,))
c2 = Process(target=consume, args=(q,))
# 启动进程
p1.start()
p2.start()
p3.start()
c1.start()
c2.start()
# 让子进程运行在父进程前,直到代码结束
p1.join()
p2.join()
p3.join()
# 有多少个消费者就得传多少个None
q.put(None)
q.put(None)
"""
多进程-demo-2
- 类: JoinableQueue([maxsize]):这就像是一个Queue对象,但队列允许项目的使用者通知生成者项目已经被成功处理。通知进程是使用共享的信号和条件变量来实现的。
参数: maxsize是队列中允许最大项数,省略则无大小限制。
方法:JoinableQueue的实例p除了与Queue对象相同的方法之外还具有:
- q.task\_done():使用者使用此方法发出信号,表示q.get()的返回项目已经被处理。如果调用此方法的次数大于从队列中删除项目的数量,将引发ValueError异常
- q.join():生产者调用此方法进行阻塞,直到队列中所有的项目均被处理。阻塞将持续到队列中的每个项目均调用q.task\_done()方法为止
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
import os
import random
import time
from multiprocessing import Process, JoinableQueue
def product(q):
for i in range(2):
time.sleep(random.randint(1, 3))
print("\033[45m{} 生产了 包子 {}\033[0m".format(os.getpid(), i))
q.put(i)
def consume(q):
while True:
time.sleep(random.randint(1, 3))
res = q.get()
if res is None: break
print("{} 吃掉了 包子 {}".format(os.getpid(), res))
q.task_done() # 1、当取走代码时,通知队列取走了一次
if __name__ == '__main__':
q = JoinableQueue()
p1 = Process(target=product, args=(q,))
p2 = Process(target=product, args=(q,))
p3 = Process(target=product, args=(q,))
c1 = Process(target=consume, args=(q,))
c2 = Process(target=consume, args=(q,))
c1.daemon = True # 守护进程需要运行在主进程之前,守护进程会在主进程代码执行结束后就终止
c2.daemon = True
p1.start()
p2.start()
p3.start()
c1.start()
c2.start()
p1.join() # 等待子进程运行完之后在执行主进程
p2.join()
p3.join()
q.join() # # 等待队列被取干净, 结束意味着主进程的代码运行完毕 -->
# 生产运行完毕并且队列中的数据也被取干净了-->
# 消费者也就没有意义了
print("master")
"""
# JoinableQueue()Queue对象,但队列允许项目的使用者通知生成者项目已经被成功处理。通知进程是使用共享的信号和条件变量来实现
1、开启子进程 Process(target=product, args=(q,)) # 传递q
2、启动子进程 p1.start() 生产者 开始生产
3、启动子进程 c1.start() 消费者 开始消费
3.1、 消费者q.task_done() # 当取走代码时,通知队列取走了一次
3.2、 q.join() 等待队列被取干净
4、待子进程运行完, 每个进程都需要join, 否则p1结束时会直接到 master进程
5、当子进程都运行完了,打印master, 子进程 设置为: daemon=True
6、当父进程都结束了,子进程也会跟着一并结束
"""
线程-demo
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
import time
import threading
import queue
q = queue.Queue(10) # 生成10个队列
# 生产者
def productor(i):
while True:
q.put("厨师做了{0}号包子".format(i))
time.sleep(2)
# 消费者
def consumer(k):
while True:
print("消费者{} 吃了{}".format(k, q.get()))
time.sleep(1)
for i in range(5):
p = threading.Thread(target=productor, args=(i,))
p.start()
for k in range(3):
c = threading.Thread(target=consumer, args=(k,))
c.start()
# 5个厨师生产, 3个生产者消费
消费者0 吃了厨师做了0号包子
消费者1 吃了厨师做了1号包子
消费者2 吃了厨师做了2号包子
消费者1 吃了厨师做了3号包子
消费者0 吃了厨师做了4号包子
...
# 最怕生产者太快,而消费者消费的速度太慢导致数据存储过慢